diff --git a/.gitignore b/.gitignore index 68d38d9ca7817..8ec1104f25535 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ atlassian* /pub/media/import/* !/pub/media/import/.htaccess /pub/media/logo/* +/pub/media/custom_options/* +!/pub/media/custom_options/.htaccess /pub/media/theme/* /pub/media/theme_customization/* !/pub/media/theme_customization/.htaccess diff --git a/.htaccess b/.htaccess index d22b5a1395cae..71a5cf708dbc5 100644 --- a/.htaccess +++ b/.htaccess @@ -27,6 +27,11 @@ #AddType x-mapp-php5 .php #AddHandler x-mapp-php5 .php +############################################ +## enable usage of methods arguments in backtrace + + SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## default index file @@ -364,6 +369,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/.htaccess.sample b/.htaccess.sample index c9ddff2cca4cf..c9e83a53cc8bd 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -27,6 +27,11 @@ #AddType x-mapp-php5 .php #AddHandler x-mapp-php5 .php +############################################ +## enable usage of methods arguments in backtrace + + SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## default index file @@ -341,6 +346,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/.travis.yml b/.travis.yml index d29aa241b15b6..76885ebab2896 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,6 @@ cache: - $HOME/node_modules - $HOME/yarn.lock before_install: - - curl -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.3.0/elasticsearch-2.3.0.deb && sudo dpkg -i --force-confnew elasticsearch-2.3.0.deb && sudo service elasticsearch restart - ./dev/travis/before_install.sh install: composer install --no-interaction before_script: ./dev/travis/before_script.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 322cd10d0ff08..0999bee6ea415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -489,7 +489,7 @@ Tests: * Fixed an issue where found records in global search in Backend could not be selected via keyboard * Fixed an issue where Category menu items went out of screen when page side was reached * Fixed an issue where subcategories in menu were shown instantly when user moved mouse quickly - * Fixed an issue where popup header was our of window range while creating group product + * Fixed an issue where popup header was out of window range while creating group product * Fixed an issue where region field was absent in customer address form on backend for "United Kingdom" country * Fixed an ability to edit the Order from Admin panel * Fixed an issue where email could not be retrieved from \Magento\Quote\Api\Data\AddressInterface after adding an address on OnePageCheckout @@ -626,7 +626,7 @@ Tests: * Fixed an issue where filters were not shown on product reviews report grid * Fixed an issue where second customer address was not deleted from customer account * Fixed an issue where custom options pop-up was still displayed after submit - * Fixed an issue where Second Product was not added to Shopping Cart from Wishlist at first atempt + * Fixed an issue where Second Product was not added to Shopping Cart from Wishlist at first attempt * Fixed an issue where customer invalid email message was not displayed * Fixed an issue where All Access Tokens for Customer without Tokens could not be revoked * Fixed an issue where it was impossible to add Product to Shopping Cart from shared Wishlist @@ -785,7 +785,7 @@ Tests: * Refactored controller actions in the Product area * Moved commands cache.php, indexer.php, log.php, test.php, compiler.php, singletenant\_compiler.php, generator.php, pack.php, deploy.php and file\_assembler.php to the new bin/magento CLI framework * Data Migration Tool - * The Data Migraiton Tool is published in the separate [repository](https://github.com/magento/data-migration-tool-ce "Data Migration Tool repository") + * The Data Migration Tool is published in the separate [repository](https://github.com/magento/data-migration-tool-ce "Data Migration Tool repository") * Fixed bugs * Fixed an issue where error appeared during placing order with virtual product * Fixed an issue where billing and shipping sections didn't contain address information on order print @@ -4136,7 +4136,7 @@ Tests: * Moved Multishipping functionality to newly created module Multishipping * Extracted Product duplication behavior from Product model to Product\Copier model * Replaced event "catalog_model_product_duplicate" with composite Product\Copier model - * Replaced event "catalog_product_prepare_save" with controller product initialization helper that can be customozed via plugins + * Replaced event "catalog_product_prepare_save" with controller product initialization helper that can be customized via plugins * Consolidated Authorize.Net functionality in single module Authorizenet * Eliminated dependency of Sales module on Shipping and Usa modules * Eliminated dependency of Shipping module on Customer module @@ -4335,7 +4335,7 @@ Tests: * Fixed order placing with virtual product using Express Checkout * Fixed the error during order placement with Recurring profile payment * Fixed wrong redirect after customer registration during multishipping checkout - * Fixed inability to crate shipping labels + * Fixed inability to create shipping labels * Fixed inability to switch language, if the default language is English * Fixed an issue with incorrect XML appearing in cache after some actions on the frontend * Fixed product export diff --git a/README.md b/README.md index e73da84d66f46..ecd457a4f1aef 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@

Welcome

Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a cutting-edge, feature-rich eCommerce solution that gets results. -## Magento system requirements -[Magento system requirements](https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements2.html). +## Magento System Requirements +[Magento System Requirements](https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements.html). ## Install Magento -* [Installation guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). +* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). -

Contributing to the Magento 2 code base

+

Contributing to the Magento 2 Code Base

Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions. To learn about how to make a contribution, click [here][1]. @@ -39,13 +39,13 @@ Magento is thankful for any contribution that can improve our code base, documen -### Labels applied by the Magento team +### Labels Applied by the Magento Team We apply labels to public Pull Requests and Issues to help other participants retrieve additional information about current progress, component assignments, Magento release lines, and much more. Please review the [Code Contributions guide](https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing.html#labels) for detailed information on labels used in Magento 2 repositories. -## Reporting security issues +## Reporting Security Issues -To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account [there](https://bugcrowd.com/magento) to submit and follow-up your issue. Learn more about reporting security issues [here](https://magento.com/security/reporting-magento-security-issue). +To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up your issue. Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications](https://magento.com/security/sign-up). diff --git a/app/bootstrap.php b/app/bootstrap.php index 0b13d12cece58..4974acdf0fc80 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -8,6 +8,9 @@ * Environment initialization */ error_reporting(E_ALL); +if (in_array('phar', \stream_get_wrappers())) { + stream_wrapper_unregister('phar'); +} #ini_set('display_errors', 1); /* PHP version validation */ diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php index 6f0e42bdcbef1..82f70d92e4930 100644 --- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php +++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php @@ -8,6 +8,11 @@ namespace Magento\AdminNotification\Block\Grid\Renderer; +/** + * Renderer class for action in the admin notifications grid + * + * @package Magento\AdminNotification\Block\Grid\Renderer + */ class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** @@ -37,7 +42,9 @@ public function __construct( */ public function render(\Magento\Framework\DataObject $row) { - $readDetailsHtml = $row->getUrl() ? '' . + $readDetailsHtml = $row->getUrl() ? '' . __('Read Details') . '' : ''; $markAsReadHtml = !$row->getIsRead() ? ' + + + + + System + Notifications + magento-backend-system + + + Notifications + Notifications + magento-adminnotification-system-adminnotification + + diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml new file mode 100644 index 0000000000000..75dceb4028622 --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + <description value="Admin should be able to navigate to System > Notifications"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14125"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemNotificationPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsNotifications.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemOtherSettingsNotifications.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml index aa8ba23d0ee59..eed6b53f34315 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml +++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="notifications"> - <uiComponent name="notification_area"/> + <uiComponent name="notification_area" aclResource="Magento_AdminNotification::show_list"/> <block class="Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup" name="unread_system_messages" as="unread_system_messages" diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 2e17e734b1e60..591e648547d61 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -185,6 +185,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract * @param AdvancedPricing\Validator\Website $websiteValidator * @param AdvancedPricing\Validator\TierPrice $tierPriceValidator * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Exception */ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, @@ -255,6 +256,7 @@ public function getEntityTypeCode() * @param array $rowData * @param int $rowNum * @return bool + * @throws \Zend_Validate_Exception */ public function validateRow(array $rowData, $rowNum) { @@ -308,6 +310,7 @@ protected function _importData() * Save advanced pricing * * @return $this + * @throws \Exception */ public function saveAdvancedPricing() { @@ -319,6 +322,7 @@ public function saveAdvancedPricing() * Deletes Advanced price data from raw data. * * @return $this + * @throws \Exception */ public function deleteAdvancedPricing() { @@ -347,6 +351,7 @@ public function deleteAdvancedPricing() * Replace advanced pricing * * @return $this + * @throws \Exception */ public function replaceAdvancedPricing() { @@ -360,6 +365,7 @@ public function replaceAdvancedPricing() * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Exception */ protected function saveAndReplaceAdvancedPrices() { @@ -368,8 +374,8 @@ protected function saveAndReplaceAdvancedPrices() $this->_cachedSkuToDelete = null; } $listSku = []; + $tierPrices = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { - $tierPrices = []; foreach ($bunch as $rowNum => $rowData) { if (!$this->validateRow($rowData, $rowNum)) { $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); @@ -397,23 +403,28 @@ protected function saveAndReplaceAdvancedPrices() ]; } } - if (\Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE == $behavior) { - if ($listSku) { - $this->processCountNewPrices($tierPrices); - if ($this->deleteProductTierPrices(array_unique($listSku), self::TABLE_TIER_PRICE)) { - $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); - $this->setUpdatedAt($listSku); - } - } - } elseif (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { + + if (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { $this->processCountExistingPrices($tierPrices, self::TABLE_TIER_PRICE) ->processCountNewPrices($tierPrices); + $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); if ($listSku) { $this->setUpdatedAt($listSku); } } } + + if (\Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE == $behavior) { + if ($listSku) { + $this->processCountNewPrices($tierPrices); + if ($this->deleteProductTierPrices(array_unique($listSku), self::TABLE_TIER_PRICE)) { + $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); + $this->setUpdatedAt($listSku); + } + } + } + return $this; } @@ -423,6 +434,7 @@ protected function saveAndReplaceAdvancedPrices() * @param array $priceData * @param string $table * @return $this + * @throws \Exception */ protected function saveProductPrices(array $priceData, $table) { @@ -454,6 +466,7 @@ protected function saveProductPrices(array $priceData, $table) * @param array $listSku * @param string $table * @return boolean + * @throws \Exception */ protected function deleteProductTierPrices(array $listSku, $table) { @@ -531,6 +544,7 @@ protected function getCustomerGroupId($customerGroup) * Retrieve product skus * * @return array + * @throws \Exception */ protected function retrieveOldSkus() { @@ -551,6 +565,7 @@ protected function retrieveOldSkus() * @param array $prices * @param string $table * @return $this + * @throws \Exception */ protected function processCountExistingPrices($prices, $table) { @@ -562,11 +577,14 @@ protected function processCountExistingPrices($prices, $table) $tableName = $this->_resourceFactory->create()->getTable($table); $productEntityLinkField = $this->getProductEntityLinkField(); - $existingPrices = $this->_connection->fetchAssoc( + $existingPrices = $this->_connection->fetchAll( $this->_connection->select()->from( $tableName, - ['value_id', $productEntityLinkField, 'all_groups', 'customer_group_id'] - )->where($productEntityLinkField . ' IN (?)', $existProductIds) + [$productEntityLinkField, 'all_groups', 'customer_group_id', 'qty'] + )->where( + $productEntityLinkField . ' IN (?)', + $existProductIds + ) ); foreach ($existingPrices as $existingPrice) { foreach ($prices as $sku => $skuPrices) { @@ -591,8 +609,10 @@ protected function incrementCounterUpdated($prices, $existingPrice) foreach ($prices as $price) { if ($existingPrice['all_groups'] == $price['all_groups'] && $existingPrice['customer_group_id'] == $price['customer_group_id'] + && (int) $existingPrice['qty'] === (int) $price['qty'] ) { $this->countItemsUpdated++; + continue; } } } diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php index 340e81746f029..2aa59c1cfb758 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php @@ -921,7 +921,7 @@ public function testProcessCountExistingPrices( ); $dbSelectMock = $this->createMock(\Magento\Framework\DB\Select::class); $this->connection->expects($this->once()) - ->method('fetchAssoc') + ->method('fetchAll') ->willReturn($existingPrices); $this->connection->expects($this->once()) ->method('select') @@ -930,7 +930,7 @@ public function testProcessCountExistingPrices( ->method('from') ->with( self::TABLE_NAME, - ['value_id', self::LINK_FIELD, 'all_groups', 'customer_group_id'] + [self::LINK_FIELD, 'all_groups', 'customer_group_id', 'qty'] )->willReturnSelf(); $this->advancedPricing->expects($this->once()) ->method('retrieveOldSkus') diff --git a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml index fa7774f5cec1d..2c4f7fca1834b 100644 --- a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml +++ b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml @@ -48,18 +48,18 @@ </depends> </field> <!--<group id="suggestions">--> - <field id="search_suggestion_enabled" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="search_suggestion_enabled" translate="label comment" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Enable Search Suggestions</label> <comment>When you enable this option your site may slow down.</comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="search_suggestion_count" translate="label" type="text" sortOrder="71" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="search_suggestion_count" translate="label" type="text" sortOrder="91" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Search Suggestions Count</label> <depends> <field id="search_suggestion_enabled">1</field> </depends> </field> - <field id="search_suggestion_count_results_enabled" translate="label" type="select" sortOrder="72" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="search_suggestion_count_results_enabled" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Show Results Count for Each Suggestion</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>When you enable this option your site may slow down.</comment> diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..01b86101def28 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?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="AdminMenuReports"> + <data key="pageTitle">Reports</data> + <data key="title">Reports</data> + <data key="dataUiId">magento-reports-report</data> + </entity> + <entity name="AdminMenuReportsBusinessIntelligenceAdvancedReporting"> + <data key="pageTitle">AdvancedReporting</data> + <data key="title">AdvancedReporting</data> + <data key="dataUiId">magento-analytics-advanced-reporting</data> + </entity> +</entities> diff --git a/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml b/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml new file mode 100644 index 0000000000000..7b6851e5bdf37 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml @@ -0,0 +1,12 @@ +<?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="AdminAdvancedReportingSection"> + <element name="goToAdvancedReporting" type="text" selector="//div[@class='dashboard-advanced-reports-actions']/a[@title='Go to Advanced Reporting']" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml new file mode 100644 index 0000000000000..e660a2eb8d428 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml @@ -0,0 +1,38 @@ +<?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="AdminAdvancedReportingButtonTest"> + <annotations> + <stories value="AdvancedReporting"/> + <title value="AdvancedReportingButtonTest"/> + <description value="Test log in to AdvancedReporting and tests AdvancedReportingButtonTest"/> + <testCaseId value="MC-14800"/> + <skip> + <issueId value="MC-14800" /> + </skip> + <severity value="CRITICAL"/> + <group value="analytics"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate through Advanced Reporting button on dashboard to Sign Up page--> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageLoad"/> + <click selector="{{AdminAdvancedReportingSection.goToAdvancedReporting}}" stepKey="clickGoToAdvancedReporting"/> + <switchToNextTab stepKey="switchToNewTab"/> + <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml new file mode 100644 index 0000000000000..67d6715285697 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml @@ -0,0 +1,35 @@ +<?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="AdminAdvancedReportingNavigateMenuTest"> + <annotations> + <features value="Analytics"/> + <stories value="Menu Navigation"/> + <title value="Admin advanced reporting navigate menu test"/> + <description value="Admin should be able to navigate through advanced reporting admin menu to BI reports page"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14152"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateAdvancedReportingPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsBusinessIntelligenceAdvancedReporting.dataUiId}}"/> + </actionGroup> + <switchToNextTab stepKey="switchToNewTab"/> + <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + </test> +</tests> diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php index 407e323aeaae6..9428f8954c60e 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php @@ -39,6 +39,15 @@ protected function setUp() ->setMethods(['getComment', 'getLabel']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + + $this->abstractElementMock->setEscaper($escaper); $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php index d567d65882350..08ee3c356937a 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -46,6 +46,14 @@ protected function setUp() ->setMethods(['getComment']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + $this->contextMock = $this->getMockBuilder(Context::class) ->setMethods(['getLocaleDate']) ->disableOriginalConstructor() diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php index 78ff581f3de9d..b43225be9570d 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -51,6 +51,14 @@ protected function setUp() ->setMethods(['getComment']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + $this->formMock = $this->getMockBuilder(Form::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php index 6a0cecc781062..0b5e86a523339 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -39,6 +39,14 @@ protected function setUp() ->setMethods(['getComment', 'getLabel', 'getHint']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php index 28bc8141a8e99..af1ef4400e442 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -9,11 +9,12 @@ namespace Magento\AsynchronousOperations\Model; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Registry; use Psr\Log\LoggerInterface; use Magento\Framework\MessageQueue\MessageLockException; use Magento\Framework\MessageQueue\ConnectionLostException; use Magento\Framework\Exception\NotFoundException; -use Magento\Framework\MessageQueue\CallbackInvoker; +use Magento\Framework\MessageQueue\CallbackInvokerInterface; use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; use Magento\Framework\MessageQueue\EnvelopeInterface; use Magento\Framework\MessageQueue\QueueInterface; @@ -29,7 +30,7 @@ class MassConsumer implements ConsumerInterface { /** - * @var \Magento\Framework\MessageQueue\CallbackInvoker + * @var CallbackInvokerInterface */ private $invoker; @@ -58,23 +59,30 @@ class MassConsumer implements ConsumerInterface */ private $operationProcessor; + /** + * @var Registry + */ + private $registry; + /** * Initialize dependencies. * - * @param CallbackInvoker $invoker + * @param CallbackInvokerInterface $invoker * @param ResourceConnection $resource * @param MessageController $messageController * @param ConsumerConfigurationInterface $configuration * @param OperationProcessorFactory $operationProcessorFactory * @param LoggerInterface $logger + * @param Registry $registry */ public function __construct( - CallbackInvoker $invoker, + CallbackInvokerInterface $invoker, ResourceConnection $resource, MessageController $messageController, ConsumerConfigurationInterface $configuration, OperationProcessorFactory $operationProcessorFactory, - LoggerInterface $logger + LoggerInterface $logger, + Registry $registry = null ) { $this->invoker = $invoker; $this->resource = $resource; @@ -84,13 +92,17 @@ public function __construct( 'configuration' => $configuration ]); $this->logger = $logger; + $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(Registry::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function process($maxNumberOfMessages = null) { + $this->registry->register('isSecureArea', true, true); + $queue = $this->configuration->getQueue(); if (!isset($maxNumberOfMessages)) { @@ -98,6 +110,8 @@ public function process($maxNumberOfMessages = null) } else { $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); } + + $this->registry->unregister('isSecureArea'); } /** diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php index eae92e1663fc8..89d468159c6e9 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -20,6 +20,7 @@ use Psr\Log\LoggerInterface; use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Encryption\Encryptor; /** * Class MassSchedule used for adding multiple entities as Operations to Bulk Management with the status tracking @@ -63,6 +64,11 @@ class MassSchedule */ private $userContext; + /** + * @var Encryptor + */ + private $encryptor; + /** * Initialize dependencies. * @@ -73,6 +79,7 @@ class MassSchedule * @param LoggerInterface $logger * @param OperationRepository $operationRepository * @param UserContextInterface $userContext + * @param Encryptor|null $encryptor */ public function __construct( IdentityGeneratorInterface $identityService, @@ -81,7 +88,8 @@ public function __construct( BulkManagementInterface $bulkManagement, LoggerInterface $logger, OperationRepository $operationRepository, - UserContextInterface $userContext = null + UserContextInterface $userContext = null, + Encryptor $encryptor = null ) { $this->identityService = $identityService; $this->itemStatusInterfaceFactory = $itemStatusInterfaceFactory; @@ -90,6 +98,7 @@ public function __construct( $this->logger = $logger; $this->operationRepository = $operationRepository; $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); + $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(Encryptor::class); } /** @@ -130,9 +139,13 @@ public function publishMass($topicName, array $entitiesArray, $groupId = null, $ $requestItem = $this->itemStatusInterfaceFactory->create(); try { - $operations[] = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $operation = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $operations[] = $operation; $requestItem->setId($key); $requestItem->setStatus(ItemStatusInterface::STATUS_ACCEPTED); + $requestItem->setDataHash( + $this->encryptor->hash($operation->getSerializedData(), Encryptor::HASH_VERSION_SHA256) + ); $requestItems[] = $requestItem; } catch (\Exception $exception) { $this->logger->error($exception); diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php index a7a670d64d7ce..c693ebe95d52b 100644 --- a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php +++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php @@ -3,13 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Block\Adminhtml\Order\View\Info; use Magento\Authorizenet\Model\Directpost; /** + * Fraud information block for Authorize.net payment method + * * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class FraudDetails extends \Magento\Backend\Block\Template { @@ -33,6 +38,8 @@ public function __construct( } /** + * Return payment method model + * * @return \Magento\Sales\Model\Order\Payment */ public function getPayment() @@ -42,6 +49,8 @@ public function getPayment() } /** + * Produce and return the block's HTML output + * * @return string */ protected function _toHtml() diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php index fb9c74d2f0ab1..23034270640dd 100644 --- a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php +++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php @@ -12,6 +12,7 @@ /** * Payment information block for Authorize.net payment method + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class PaymentDetails extends ConfigurableInfo { diff --git a/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php b/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php index 296d22d6f61b2..65161413cb18f 100644 --- a/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php +++ b/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Block\Transparent; use Magento\Payment\Block\Transparent\Iframe as TransparentIframe; /** + * Transparent Iframe block for Authorize.net payments * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Iframe extends TransparentIframe { diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php index 46d395b978eba..f71314613fc1f 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class AddConfigured extends \Magento\Sales\Controller\Adminhtml\Order\Create\AddConfigured +use Magento\Framework\App\Action\HttpPutActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\AddConfigured as BaseAddConfigured; + +/** + * Class AddConfigured + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class AddConfigured extends BaseAddConfigured implements HttpPutActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php index 3432e14d77b9e..3ebea4704db7e 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class Cancel extends \Magento\Sales\Controller\Adminhtml\Order\Create\Cancel +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\Cancel as BaseCancel; + +/** + * Class Cancel + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class Cancel extends BaseCancel implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php index 9fa3c7dd19b88..19eb4571a852e 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ConfigureProductToAdd extends \Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureProductToAdd +use Magento\Framework\App\Action\HttpPutActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureProductToAdd as BaseConfigureProductToAdd; + +/** + * Class ConfigureProductToAdd + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ConfigureProductToAdd extends BaseConfigureProductToAdd implements HttpPutActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php index c1ea98aea2382..d314149059c72 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ConfigureQuoteItems extends \Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureQuoteItems +use Magento\Framework\App\Action\HttpPutActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureQuoteItems as BaseConfigureQuoteItems; + +/** + * Class ConfigureQuoteItems + * @deprecated 2.3 Authorize.net is removing all support for this payment method + */ +class ConfigureQuoteItems extends BaseConfigureQuoteItems implements HttpPutActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php index b206f89ab8bf5..33ac620499e71 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class Index + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create\Index { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php index 43e456e766932..577840c0a9ba4 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class LoadBlock + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class LoadBlock extends \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php index b393015ce1231..fc4cce07bd08f 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php @@ -3,21 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -use Magento\Framework\Escaper; -use Magento\Catalog\Helper\Product; +use Magento\Authorizenet\Helper\Backend\Data as DataHelper; use Magento\Backend\App\Action\Context; -use Magento\Framework\View\Result\PageFactory; use Magento\Backend\Model\View\Result\ForwardFactory; -use Magento\Authorizenet\Helper\Backend\Data as DataHelper; +use Magento\Catalog\Helper\Product; +use Magento\Framework\Escaper; +use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; /** * Class Place * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Place extends \Magento\Sales\Controller\Adminhtml\Order\Create +class Place extends Create implements HttpPostActionInterface { /** * @var DataHelper diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php index 35720249be359..3d0d572bd6265 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ProcessData extends \Magento\Sales\Controller\Adminhtml\Order\Create\ProcessData +use Magento\Sales\Controller\Adminhtml\Order\Create\ProcessData as BaseProcessData; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class ProcessData + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ProcessData extends BaseProcessData implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php index bd9de956dc647..333751f93653a 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; use Magento\Backend\App\Action; @@ -10,11 +12,16 @@ use Magento\Framework\View\Result\LayoutFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Payment\Block\Transparent\Iframe; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; /** + * Class Redirect * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Redirect extends \Magento\Sales\Controller\Adminhtml\Order\Create +class Redirect extends Create implements HttpGetActionInterface, HttpPostActionInterface { /** * Core registry diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php index 80b9f54524f00..06a6403915ff1 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class Reorder extends \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\Reorder as BaseReorder; + +/** + * Class Reorder + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class Reorder extends BaseReorder implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php index 82a5ee08f7ce8..c42e7ecbeef00 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php @@ -4,9 +4,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ReturnQuote extends \Magento\Sales\Controller\Adminhtml\Order\Create +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; + +/** + * Class ReturnQuote + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ReturnQuote extends Create implements HttpPostActionInterface, HttpGetActionInterface { /** * Return quote diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php index 7519f3415c40b..cc93ce5daedeb 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class Save + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create\Save { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php index b55da878b2e39..af80bde10831a 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class ShowUpdateResult + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class ShowUpdateResult extends \Magento\Sales\Controller\Adminhtml\Order\Create\ShowUpdateResult { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php index f0ac5f80c11ab..689b30d63be68 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -abstract class Start extends \Magento\Sales\Controller\Adminhtml\Order\Create +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; + +/** + * Class Start + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +abstract class Start extends Create implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php index fc9e7807cd99e..cfaa5f1cfcd08 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php @@ -3,16 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Payment\Block\Transparent\Iframe; +use Magento\Framework\App\Action\Action; /** * DirectPost Payment Controller * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -abstract class Payment extends \Magento\Framework\App\Action\Action +abstract class Payment extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * Core registry @@ -44,6 +50,8 @@ public function __construct( } /** + * Get checkout model + * * @return \Magento\Checkout\Model\Session */ protected function _getCheckout() @@ -63,6 +71,7 @@ protected function _getDirectPostSession() /** * Response action. + * * Action for Authorize.net SIM Relay Request. * * @param string $area diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php index 70565ea8ac65f..e0610a92feb6a 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php @@ -4,6 +4,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Authorizenet\Helper\DataFactory; @@ -16,9 +18,18 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Psr\Log\LoggerInterface; -class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment implements CsrfAwareActionInterface +/** + * Class BackendResponse + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment implements + CsrfAwareActionInterface, + HttpGetActionInterface, + HttpPostActionInterface { /** * @var LoggerInterface @@ -70,6 +81,7 @@ public function validateForCsrf(RequestInterface $request): ?bool /** * Response action. + * * Action for Authorize.net SIM Relay Request. * * @return \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php index 3c1cb90e0c0a5..7d672a75f5b17 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Authorizenet\Controller\Directpost\Payment; @@ -25,6 +26,7 @@ * Class Place * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Place extends Payment implements HttpPostActionInterface { diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php index 028b90bf7da50..8c9510243f610 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php @@ -4,15 +4,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; -use Magento\Framework\App\ObjectManager; +use Magento\Authorizenet\Controller\Directpost\Payment; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Payment\Block\Transparent\Iframe; /** * Class Redirect + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Redirect extends \Magento\Authorizenet\Controller\Directpost\Payment +class Redirect extends Payment implements HttpGetActionInterface, HttpPostActionInterface { /** * Retrieve params and put javascript into iframe diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php index d562df9fb24a9..17fc3cb72e454 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php @@ -4,13 +4,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Framework\App\CsrfAwareActionInterface; use Magento\Framework\App\Request\InvalidRequestException; use Magento\Framework\App\RequestInterface; +use Magento\Authorizenet\Controller\Directpost\Payment; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; -class Response extends \Magento\Authorizenet\Controller\Directpost\Payment implements CsrfAwareActionInterface +/** + * Class Response + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class Response extends Payment implements CsrfAwareActionInterface, HttpGetActionInterface, HttpPostActionInterface { /** * @inheritDoc @@ -31,6 +40,7 @@ public function validateForCsrf(RequestInterface $request): ?bool /** * Response action. + * * Action for Authorize.net SIM Relay Request. * * @return \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php index 3030a75055b7e..c974632f584b0 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php @@ -4,9 +4,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; -class ReturnQuote extends \Magento\Authorizenet\Controller\Directpost\Payment +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Authorizenet\Controller\Directpost\Payment; + +/** + * Class ReturnQuote + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ReturnQuote extends Payment implements HttpPostActionInterface, HttpGetActionInterface { /** * Return customer quote by ajax diff --git a/app/code/Magento/Authorizenet/Helper/Backend/Data.php b/app/code/Magento/Authorizenet/Helper/Backend/Data.php index 24bdb23873265..d291125ccae06 100644 --- a/app/code/Magento/Authorizenet/Helper/Backend/Data.php +++ b/app/code/Magento/Authorizenet/Helper/Backend/Data.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Helper\Backend; use Magento\Authorizenet\Helper\Data as FrontendDataHelper; @@ -16,6 +18,7 @@ * * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Data extends FrontendDataHelper { diff --git a/app/code/Magento/Authorizenet/Helper/Data.php b/app/code/Magento/Authorizenet/Helper/Data.php index 8bcc1f3f3f03c..e240cd692a13f 100644 --- a/app/code/Magento/Authorizenet/Helper/Data.php +++ b/app/code/Magento/Authorizenet/Helper/Data.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Helper; use Magento\Framework\App\Helper\AbstractHelper; @@ -17,6 +19,7 @@ * * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Data extends AbstractHelper { @@ -153,6 +156,7 @@ public function getSuccessOrderUrl($params) /** * Update all child and parent order's edit increment numbers. + * * Needed for Admin area. * * @param \Magento\Sales\Model\Order $order @@ -255,6 +259,7 @@ protected function getOperation($requestType) /** * Format price with currency sign + * * @param \Magento\Payment\Model\InfoInterface $payment * @param float $amount * @return string diff --git a/app/code/Magento/Authorizenet/Helper/DataFactory.php b/app/code/Magento/Authorizenet/Helper/DataFactory.php index f3ccf16e7d396..71f16ab4af646 100644 --- a/app/code/Magento/Authorizenet/Helper/DataFactory.php +++ b/app/code/Magento/Authorizenet/Helper/DataFactory.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Helper; use Magento\Framework\Exception\LocalizedException; @@ -10,6 +12,7 @@ /** * Class DataFactory + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class DataFactory { diff --git a/app/code/Magento/Authorizenet/Model/Authorizenet.php b/app/code/Magento/Authorizenet/Model/Authorizenet.php index ae9ac833a4395..9370b649a23c7 100644 --- a/app/code/Magento/Authorizenet/Model/Authorizenet.php +++ b/app/code/Magento/Authorizenet/Model/Authorizenet.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Authorizenet\Model\TransactionService; use Magento\Framework\HTTP\ZendClientFactory; /** + * Model for Authorize.net payment method + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ abstract class Authorizenet extends \Magento\Payment\Model\Method\Cc { diff --git a/app/code/Magento/Authorizenet/Model/Debug.php b/app/code/Magento/Authorizenet/Model/Debug.php index 255c2e3aba444..93d508cc744e1 100644 --- a/app/code/Magento/Authorizenet/Model/Debug.php +++ b/app/code/Magento/Authorizenet/Model/Debug.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; /** + * Authorize.net debug payment method model + * * @method string getRequestBody() * @method \Magento\Authorizenet\Model\Debug setRequestBody(string $value) * @method string getResponseBody() @@ -18,10 +22,13 @@ * @method \Magento\Authorizenet\Model\Debug setRequestDump(string $value) * @method string getResultDump() * @method \Magento\Authorizenet\Model\Debug setResultDump(string $value) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Debug extends \Magento\Framework\Model\AbstractModel { /** + * Construct debug class + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index aeaa0cd9a3ad1..946ec8ba01a0e 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Framework\App\ObjectManager; @@ -14,6 +16,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements TransparentInterface, ConfigInterface { @@ -543,15 +546,16 @@ public function setResponseData(array $postData) public function validateResponse() { $response = $this->getResponse(); - //md5 check - if (!$this->getConfigData('trans_md5') - || !$this->getConfigData('login') - || !$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login')) + $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; + + //hash check + if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) ) { throw new \Magento\Framework\Exception\LocalizedException( __('The transaction was declined because the response hash validation failed.') ); } + return true; } @@ -651,7 +655,7 @@ public function checkResponseCode() case self::RESPONSE_CODE_ERROR: $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); $order = $this->getOrderFromResponse(); - $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage); + $this->paymentFailures->handle((int)$order->getQuoteId(), (string)$errorMessage); throw new \Magento\Framework\Exception\LocalizedException($errorMessage); default: throw new \Magento\Framework\Exception\LocalizedException( diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php index fc78d836b6080..10be4cd5febf6 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Authorizenet\Model\Directpost; use Magento\Authorizenet\Model\Request as AuthorizenetRequest; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Intl\DateTimeFactory; /** * Authorize.net request model for DirectPost model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Request extends AuthorizenetRequest { @@ -18,9 +22,35 @@ class Request extends AuthorizenetRequest */ protected $_transKey = null; + /** + * Hexadecimal signature key. + * + * @var string + */ + private $signatureKey = ''; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param array $data + * @param DateTimeFactory $dateTimeFactory + */ + public function __construct( + array $data = [], + DateTimeFactory $dateTimeFactory = null + ) { + $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance() + ->get(DateTimeFactory::class); + parent::__construct($data); + } + /** * Return merchant transaction key. - * Needed to generate sign. + * + * Needed to generate MD5 sign. * * @return string */ @@ -31,7 +61,8 @@ protected function _getTransactionKey() /** * Set merchant transaction key. - * Needed to generate sign. + * + * Needed to generate MD5 sign. * * @param string $transKey * @return $this @@ -43,7 +74,7 @@ protected function _setTransactionKey($transKey) } /** - * Generates the fingerprint for request. + * Generates the MD5 fingerprint for request. * * @param string $merchantApiLoginId * @param string $merchantTransactionKey @@ -63,7 +94,7 @@ public function generateRequestSign( ) { return hash_hmac( "md5", - $merchantApiLoginId . "^" . $fpSequence . "^" . $fpTimestamp . "^" . $amount . "^" . $currencyCode, + $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode, $merchantTransactionKey ); } @@ -78,7 +109,7 @@ public function setConstantData(\Magento\Authorizenet\Model\Directpost $paymentM { $this->setXVersion('3.1')->setXDelimData('FALSE')->setXRelayResponse('TRUE'); - $this->setXTestRequest($paymentMethod->getConfigData('test') ? 'TRUE' : 'FALSE'); + $this->setSignatureKey($paymentMethod->getConfigData('signature_key')); $this->setXLogin($paymentMethod->getConfigData('login')) ->setXMethod(\Magento\Authorizenet\Model\Authorizenet::REQUEST_METHOD_CC) @@ -162,23 +193,88 @@ public function setDataFromOrder( /** * Set sign hash into the request object. - * All needed fields should be placed in the object fist. + * + * All needed fields should be placed in the object first. * * @return $this */ public function signRequestData() { - $fpTimestamp = time(); - $hash = $this->generateRequestSign( - $this->getXLogin(), - $this->_getTransactionKey(), - $this->getXAmount(), - $this->getXCurrencyCode(), - $this->getXFpSequence(), - $fpTimestamp - ); + $fpDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $fpTimestamp = $fpDate->getTimestamp(); + + if (!empty($this->getSignatureKey())) { + $hash = $this->generateSha2RequestSign( + (string)$this->getXLogin(), + (string)$this->getSignatureKey(), + (string)$this->getXAmount(), + (string)$this->getXCurrencyCode(), + (string)$this->getXFpSequence(), + $fpTimestamp + ); + } else { + $hash = $this->generateRequestSign( + $this->getXLogin(), + $this->_getTransactionKey(), + $this->getXAmount(), + $this->getXCurrencyCode(), + $this->getXFpSequence(), + $fpTimestamp + ); + } + $this->setXFpTimestamp($fpTimestamp); $this->setXFpHash($hash); + return $this; } + + /** + * Generates the SHA2 fingerprint for request. + * + * @param string $merchantApiLoginId + * @param string $merchantSignatureKey + * @param string $amount + * @param string $currencyCode + * @param string $fpSequence An invoice number or random number. + * @param int $fpTimestamp + * @return string The fingerprint. + */ + private function generateSha2RequestSign( + string $merchantApiLoginId, + string $merchantSignatureKey, + string $amount, + string $currencyCode, + string $fpSequence, + int $fpTimestamp + ): string { + $message = $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode; + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantSignatureKey))); + } + + /** + * Return merchant hexadecimal signature key. + * + * Needed to generate SHA2 sign. + * + * @return string + */ + private function getSignatureKey(): string + { + return $this->signatureKey; + } + + /** + * Set merchant hexadecimal signature key. + * + * Needed to generate SHA2 sign. + * + * @param string $signatureKey + * @return void + */ + private function setSignatureKey(string $signatureKey) + { + $this->signatureKey = $signatureKey; + } } diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php index 2cdd02d7f8488..6036935f57be1 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost\Request; use Magento\Authorizenet\Model\Request\Factory as AuthorizenetRequestFactory; /** * Factory class for @see \Magento\Authorizenet\Model\Directpost\Request + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory extends AuthorizenetRequestFactory { diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response.php b/app/code/Magento/Authorizenet/Model/Directpost/Response.php index dc62c1e990dc3..b5604a78cb9cd 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Response.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Response.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost; use Magento\Authorizenet\Model\Response as AuthorizenetResponse; @@ -10,6 +12,7 @@ /** * Authorize.net response model for DirectPost model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Response extends AuthorizenetResponse { @@ -24,25 +27,31 @@ class Response extends AuthorizenetResponse */ public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) { - if (!$amount) { - $amount = '0.00'; - } - return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); } /** * Return if is valid order id. * - * @param string $merchantMd5 + * @param string $storedHash * @param string $merchantApiLogin * @return bool */ - public function isValidHash($merchantMd5, $merchantApiLogin) + public function isValidHash($storedHash, $merchantApiLogin) { - $hash = $this->generateHash($merchantMd5, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); + if (empty($this->getData('x_amount'))) { + $this->setData('x_amount', '0.00'); + } - return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); + if (!empty($this->getData('x_SHA2_Hash'))) { + $hash = $this->generateSha2Hash($storedHash); + return Security::compareStrings($hash, $this->getData('x_SHA2_Hash')); + } elseif (!empty($this->getData('x_MD5_Hash'))) { + $hash = $this->generateHash($storedHash, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); + return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); + } + + return false; } /** @@ -54,4 +63,54 @@ public function isApproved() { return $this->getXResponseCode() == \Magento\Authorizenet\Model\Directpost::RESPONSE_CODE_APPROVED; } + + /** + * Generates an SHA2 hash to compare against AuthNet's. + * + * @param string $signatureKey + * @return string + * @see https://support.authorize.net/s/article/MD5-Hash-End-of-Life-Signature-Key-Replacement + */ + private function generateSha2Hash(string $signatureKey): string + { + $hashFields = [ + 'x_trans_id', + 'x_test_request', + 'x_response_code', + 'x_auth_code', + 'x_cvv2_resp_code', + 'x_cavv_response', + 'x_avs_code', + 'x_method', + 'x_account_number', + 'x_amount', + 'x_company', + 'x_first_name', + 'x_last_name', + 'x_address', + 'x_city', + 'x_state', + 'x_zip', + 'x_country', + 'x_phone', + 'x_fax', + 'x_email', + 'x_ship_to_company', + 'x_ship_to_first_name', + 'x_ship_to_last_name', + 'x_ship_to_address', + 'x_ship_to_city', + 'x_ship_to_state', + 'x_ship_to_zip', + 'x_ship_to_country', + 'x_invoice_num', + ]; + + $message = '^'; + foreach ($hashFields as $field) { + $message .= ($this->getData($field) ?? '') . '^'; + } + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); + } } diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php index c2a24ef386ab0..4fda5ac62b498 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost\Response; use Magento\Authorizenet\Model\Response\Factory as AuthorizenetResponseFactory; /** * Factory class for @see \Magento\Authorizenet\Model\Directpost\Response + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory extends AuthorizenetResponseFactory { diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Session.php b/app/code/Magento/Authorizenet/Model/Directpost/Session.php index 7ddedac161399..26c5ff0cb7e36 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Session.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Session.php @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost; use Magento\Framework\Session\SessionManager; /** * Authorize.net DirectPost session model + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Session extends SessionManager { diff --git a/app/code/Magento/Authorizenet/Model/Request.php b/app/code/Magento/Authorizenet/Model/Request.php index dc52f84baecee..552439fc8bb9b 100644 --- a/app/code/Magento/Authorizenet/Model/Request.php +++ b/app/code/Magento/Authorizenet/Model/Request.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Framework\DataObject; /** * Request object + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Request extends DataObject { diff --git a/app/code/Magento/Authorizenet/Model/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Request/Factory.php index e60bbd0c88e83..a7a636280e28d 100644 --- a/app/code/Magento/Authorizenet/Model/Request/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Request/Factory.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Request; /** * Factory class for @see \Magento\Authorizenet\Model\Request + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory { diff --git a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php index ee6ec6783bb06..2c21d0e2e28e0 100644 --- a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php +++ b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\ResourceModel; /** * Resource Authorize.net debug model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Debug extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php index 095ac5a91dd7c..b84ee1e72a2d4 100644 --- a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php +++ b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\ResourceModel\Debug; /** * Resource Authorize.net debug collection model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Authorizenet/Model/Response.php b/app/code/Magento/Authorizenet/Model/Response.php index 52b43c251dca2..c552663a15373 100644 --- a/app/code/Magento/Authorizenet/Model/Response.php +++ b/app/code/Magento/Authorizenet/Model/Response.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Framework\DataObject; /** * Response object + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Response extends DataObject { diff --git a/app/code/Magento/Authorizenet/Model/Response/Factory.php b/app/code/Magento/Authorizenet/Model/Response/Factory.php index 74bf8953471d2..4578095566004 100644 --- a/app/code/Magento/Authorizenet/Model/Response/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Response/Factory.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Response; /** * Factory class for @see \Magento\Authorizenet\Model\Response + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory { diff --git a/app/code/Magento/Authorizenet/Model/Source/Cctype.php b/app/code/Magento/Authorizenet/Model/Source/Cctype.php index 0a5fe8ab9b341..ffb3584722450 100644 --- a/app/code/Magento/Authorizenet/Model/Source/Cctype.php +++ b/app/code/Magento/Authorizenet/Model/Source/Cctype.php @@ -3,16 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Source; use Magento\Payment\Model\Source\Cctype as PaymentCctype; /** * Authorize.net Payment CC Types Source Model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Cctype extends PaymentCctype { /** + * Return all supported credit card types + * * @return string[] */ public function getAllowedTypes() diff --git a/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php b/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php index 9943e1001da56..c6e57557f65c5 100644 --- a/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php +++ b/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php @@ -3,18 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Source; use Magento\Framework\Option\ArrayInterface; /** - * * Authorize.net Payment Action Dropdown source + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class PaymentAction implements ArrayInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function toOptionArray() { diff --git a/app/code/Magento/Authorizenet/Model/TransactionService.php b/app/code/Magento/Authorizenet/Model/TransactionService.php index 693a5b890faba..af0b02e94cf45 100644 --- a/app/code/Magento/Authorizenet/Model/TransactionService.php +++ b/app/code/Magento/Authorizenet/Model/TransactionService.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Authorizenet\Model; @@ -15,7 +16,7 @@ /** * Class TransactionService - * @package Magento\Authorizenet\Model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class TransactionService { @@ -74,6 +75,7 @@ public function __construct( /** * Get transaction information + * * @param \Magento\Authorizenet\Model\Authorizenet $context * @param string $transactionId * @return \Magento\Framework\Simplexml\Element @@ -142,6 +144,7 @@ protected function loadTransactionDetails(Authorizenet $context, $transactionId) /** * Create request body to get transaction details + * * @param string $login * @param string $transactionKey * @param string $transactionId diff --git a/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php b/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php index 03846dddfdee3..bdd10437927c8 100644 --- a/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php +++ b/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Model\Order; +/** + * Class AddFieldsToResponseObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class AddFieldsToResponseObserver implements ObserverInterface { /** diff --git a/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php b/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php index 8426d004c2037..45f0adfa96f4f 100644 --- a/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php +++ b/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Model\Order; +/** + * Class SaveOrderAfterSubmitObserver + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class SaveOrderAfterSubmitObserver implements ObserverInterface { /** diff --git a/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php b/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php index 3e62fe2278d3b..d6cc51eb63c01 100644 --- a/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php +++ b/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Model\Order; +/** + * Class UpdateAllEditIncrementsObserver + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class UpdateAllEditIncrementsObserver implements ObserverInterface { /** diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php new file mode 100644 index 0000000000000..94d8f3a0d27a7 --- /dev/null +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Authorizenet\Test\Unit\Model\Directpost; + +use Magento\Authorizenet\Model\Directpost\Request; +use Magento\Framework\Intl\DateTimeFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class RequestTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DateTimeFactory|MockObject + */ + private $dateTimeFactory; + + /** + * @var Request + */ + private $requestModel; + + protected function setUp() + { + $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $dateTime = new \DateTime('2016-07-05 00:00:00', new \DateTimeZone('UTC')); + $this->dateTimeFactory->method('create') + ->willReturn($dateTime); + + $this->requestModel = new Request([], $this->dateTimeFactory); + } + + /** + * @param string $signatureKey + * @param string $expectedHash + * @dataProvider signRequestDataProvider + */ + public function testSignRequestData(string $signatureKey, string $expectedHash) + { + /** @var \Magento\Authorizenet\Model\Directpost $paymentMethod */ + $paymentMethod = $this->createMock(\Magento\Authorizenet\Model\Directpost::class); + $paymentMethod->method('getConfigData') + ->willReturnMap( + [ + ['test', null, true], + ['login', null, 'login'], + ['trans_key', null, 'trans_key'], + ['signature_key', null, $signatureKey], + ] + ); + + $this->requestModel->setConstantData($paymentMethod); + $this->requestModel->signRequestData(); + $signHash = $this->requestModel->getXFpHash(); + + $this->assertEquals($expectedHash, $signHash); + } + + /** + * @return array + */ + public function signRequestDataProvider() + { + return [ + [ + 'signatureKey' => '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF65' . + '70C8C29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F', + 'expectedHash' => '719ED94DF5CF3510CB5531E8115462C8F12CBCC8E917BD809E8D40B4FF06' . + '1E14953554403DD9813CCCE0F31B184EB4DEF558E9C0747505A0C25420372DB00BE1' + ], + [ + 'signatureKey' => '', + 'expectedHash' => '3656211f2c41d1e4c083606f326c0460' + ], + ]; + } +} diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php index 15c7eecb09a69..ff4aa8b5ee361 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php @@ -13,53 +13,16 @@ class ResponseTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Authorizenet\Model\Directpost\Response */ - protected $responseModel; + private $responseModel; protected function setUp() { $objectManager = new ObjectManager($this); - $this->responseModel = $objectManager->getObject(\Magento\Authorizenet\Model\Directpost\Response::class); - } - - /** - * @param string $merchantMd5 - * @param string $merchantApiLogin - * @param float|null $amount - * @param float|string $amountTestFunc - * @param string $transactionId - * @dataProvider generateHashDataProvider - */ - public function testGenerateHash($merchantMd5, $merchantApiLogin, $amount, $amountTestFunc, $transactionId) - { - $this->assertEquals( - $this->generateHash($merchantMd5, $merchantApiLogin, $amountTestFunc, $transactionId), - $this->responseModel->generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) + $this->responseModel = $objectManager->getObject( + \Magento\Authorizenet\Model\Directpost\Response::class ); } - /** - * @return array - */ - public function generateHashDataProvider() - { - return [ - [ - 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', - 'merchantApiLogin' => 'Magento', - 'amount' => null, - 'amountTestFunc' => '0.00', - 'transactionId' => '1' - ], - [ - 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', - 'merchantApiLogin' => 'Magento2', - 'amount' => 100.50, - 'amountTestFunc' => 100.50, - 'transactionId' => '2' - ] - ]; - } - /** * @param $merchantMd5 * @param $merchantApiLogin @@ -73,7 +36,8 @@ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $trans } /** - * @param string $merchantMd5 + * @param string $storedHash + * @param string $hashKey * @param string $merchantApiLogin * @param float|null $amount * @param string $transactionId @@ -81,12 +45,21 @@ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $trans * @param bool $expectedValue * @dataProvider isValidHashDataProvider */ - public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $transactionId, $hash, $expectedValue) - { + public function testIsValidHash( + string $storedHash, + string $hashKey, + string $merchantApiLogin, + $amount, + string $transactionId, + string $hash, + bool $expectedValue + ) { $this->responseModel->setXAmount($amount); $this->responseModel->setXTransId($transactionId); - $this->responseModel->setData('x_MD5_Hash', $hash); - $this->assertEquals($expectedValue, $this->responseModel->isValidHash($merchantMd5, $merchantApiLogin)); + $this->responseModel->setData($hashKey, $hash); + $result = $this->responseModel->isValidHash($storedHash, $merchantApiLogin); + + $this->assertEquals($expectedValue, $result); } /** @@ -94,9 +67,14 @@ public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $trans */ public function isValidHashDataProvider() { + $signatureKey = '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF6570C8C' . + '29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F'; + $expectedSha2Hash = '368D48E0CD1274BF41C059138DA69985594021A4AD5B4C5526AE88C8F' . + '7C5769B13C5E1E4358900F3E51076FB69D14B0A797904C22E8A11A52AA49CDE5FBB703C'; return [ [ 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', + 'hashKey' => 'x_MD5_Hash', 'merchantApiLogin' => 'Magento', 'amount' => null, 'transactionId' => '1', @@ -105,11 +83,21 @@ public function isValidHashDataProvider() ], [ 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', + 'hashKey' => 'x_MD5_Hash', 'merchantApiLogin' => 'Magento2', 'amount' => 100.50, 'transactionId' => '2', 'hash' => '1F24A4EC9A169B2B2A072A5F168E16DC', 'expectedValue' => false + ], + [ + 'signatureKey' => $signatureKey, + 'hashKey' => 'x_SHA2_Hash', + 'merchantApiLogin' => 'Magento2', + 'amount' => 100.50, + 'transactionId' => '2', + 'hash' => $expectedSha2Hash, + 'expectedValue' => true ] ]; } diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml index 1319fa102d0d8..fc86c0d2dc68d 100644 --- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml +++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml @@ -9,7 +9,7 @@ <system> <section id="payment"> <group id="authorizenet_directpost" translate="label" type="text" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Authorize.net Direct Post</label> + <label>Authorize.Net Direct Post (Deprecated)</label> <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> @@ -29,6 +29,10 @@ <label>Transaction Key</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> </field> + <field id="signature_key" translate="label" type="obscure" sortOrder="55" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Signature Key</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> <field id="trans_md5" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Merchant MD5</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml index a960e96a288ea..60356460f553f 100644 --- a/app/code/Magento/Authorizenet/etc/config.xml +++ b/app/code/Magento/Authorizenet/etc/config.xml @@ -19,9 +19,10 @@ <order_status>processing</order_status> <payment_action>authorize</payment_action> <test>1</test> - <title>Credit Card Direct Post (Authorize.net) + Credit Card Direct Post (Authorize.Net) + 0 USD 1 diff --git a/app/code/Magento/Authorizenet/etc/payment.xml b/app/code/Magento/Authorizenet/etc/payment.xml index 8f92d5fba3aef..1d2cac374d8dc 100644 --- a/app/code/Magento/Authorizenet/etc/payment.xml +++ b/app/code/Magento/Authorizenet/etc/payment.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd"> - + diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv index 6228d5102b13c..d724bd960d310 100644 --- a/app/code/Magento/Authorizenet/i18n/en_US.csv +++ b/app/code/Magento/Authorizenet/i18n/en_US.csv @@ -45,7 +45,7 @@ void,void "Fraud Filters","Fraud Filters" "Place Order","Place Order" "Sorry, but something went wrong. Please contact the seller.","Sorry, but something went wrong. Please contact the seller." -"Authorize.net Direct Post","Authorize.net Direct Post" +"Authorize.Net Direct Post (Deprecated)","Authorize.Net Direct Post (Deprecated)" Enabled,Enabled "Payment Action","Payment Action" Title,Title diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php new file mode 100644 index 0000000000000..9f10b2df40e9f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php @@ -0,0 +1,62 @@ +config = $config; + $this->sessionQuote = $sessionQuote; + } + + /** + * Check if cvv validation is available + * + * @return boolean + */ + public function isCvvEnabled(): bool + { + return $this->config->isCvvEnabled($this->sessionQuote->getStoreId()); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php new file mode 100644 index 0000000000000..ea476eaa55716 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php @@ -0,0 +1,31 @@ +config = $config; + $this->json = $json; + } + + /** + * Retrieves the config that should be used by the block + * + * @return string + */ + public function getPaymentConfig(): string + { + $payment = $this->config->getConfig()['payment']; + $config = $payment[$this->getMethodCode()]; + $config['code'] = $this->getMethodCode(); + + return $this->json->serialize($config); + } + + /** + * Returns the method code for this payment method + * + * @return string + */ + public function getMethodCode(): string + { + return Config::METHOD; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php new file mode 100644 index 0000000000000..a72435644d23c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php @@ -0,0 +1,74 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): void + { + if ($this->shouldAcceptInGateway($commandSubject)) { + $this->commandPool->get(self::ACCEPT_FDS) + ->execute($commandSubject); + } + } + + /** + * Determines if the transaction needs to be accepted in the gateway + * + * @param array $commandSubject + * @return bool + * @throws CommandException + */ + private function shouldAcceptInGateway(array $commandSubject): bool + { + $details = $this->commandPool->get('get_transaction_details') + ->execute($commandSubject) + ->get(); + + return in_array($details['transaction']['transactionStatus'], self::NEEDS_APPROVAL_STATUSES); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php new file mode 100644 index 0000000000000..a4d895d4daae0 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php @@ -0,0 +1,140 @@ +commandPool = $commandPool; + $this->transactionRepository = $repository; + $this->filterBuilder = $filterBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): void + { + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($commandSubject); + + $command = $this->getCommand($paymentDO); + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Get execution command name. + * + * @param PaymentDataObjectInterface $paymentDO + * @return string + */ + private function getCommand(PaymentDataObjectInterface $paymentDO): string + { + $payment = $paymentDO->getPayment(); + ContextHelper::assertOrderPayment($payment); + + // If auth transaction does not exist then execute authorize&capture command + $captureExists = $this->captureTransactionExists($payment); + if (!$payment->getAuthorizationTransaction() && !$captureExists) { + return self::SALE; + } + + return self::CAPTURE; + } + + /** + * Check if capture transaction already exists + * + * @param OrderPaymentInterface $payment + * @return bool + */ + private function captureTransactionExists(OrderPaymentInterface $payment): bool + { + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('payment_id') + ->setValue($payment->getId()) + ->create(), + ] + ); + + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('txn_type') + ->setValue(TransactionInterface::TYPE_CAPTURE) + ->create(), + ] + ); + + $searchCriteria = $this->searchCriteriaBuilder->create(); + $count = $this->transactionRepository->getList($searchCriteria) + ->getTotalCount(); + + return $count > 0; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php new file mode 100644 index 0000000000000..bb9e7c26a45b1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php @@ -0,0 +1,87 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + $this->config = $config; + $this->handler = $handler; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): array + { + $paymentDO = $this->subjectReader->readPayment($commandSubject); + $order = $paymentDO->getOrder(); + + $command = $this->commandPool->get('get_transaction_details'); + $result = $command->execute($commandSubject); + $response = $result->get(); + + if ($this->handler) { + $this->handler->handle($commandSubject, $response); + } + + $additionalInformationKeys = $this->config->getTransactionInfoSyncKeys($order->getStoreId()); + $rawDetails = []; + foreach ($additionalInformationKeys as $key) { + if (isset($response['transaction'][$key])) { + $rawDetails[$key] = $response['transaction'][$key]; + } + } + + return $rawDetails; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php new file mode 100644 index 0000000000000..f8975ef38eed1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php @@ -0,0 +1,100 @@ +requestBuilder = $requestBuilder; + $this->transferFactory = $transferFactory; + $this->client = $client; + $this->validator = $validator; + $this->logger = $logger; + } + + /** + * @inheritdoc + * + * @throws Exception + */ + public function execute(array $commandSubject): ResultInterface + { + $transferO = $this->transferFactory->create( + $this->requestBuilder->build($commandSubject) + ); + + try { + $response = $this->client->placeRequest($transferO); + } catch (Exception $e) { + $this->logger->critical($e); + + throw new CommandException(__('There was an error while trying to process the request.')); + } + + $result = $this->validator->validate( + array_merge($commandSubject, ['response' => $response]) + ); + if (!$result->isValid()) { + throw new CommandException(__('There was an error while trying to process the request.')); + } + + return new ArrayResult($response); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php new file mode 100644 index 0000000000000..53a1f13fa8786 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php @@ -0,0 +1,77 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): void + { + $command = $this->getCommand($commandSubject); + + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Determines the command that should be used based on the status of the transaction + * + * @param array $commandSubject + * @return string + * @throws CommandException + */ + private function getCommand(array $commandSubject): string + { + $details = $this->commandPool->get('get_transaction_details') + ->execute($commandSubject) + ->get(); + + if ($details['transaction']['transactionStatus'] === 'capturedPendingSettlement') { + return self::VOID; + } elseif ($details['transaction']['transactionStatus'] !== 'settledSuccessfully') { + throw new CommandException(__('This transaction cannot be refunded with its current status.')); + } + + return self::REFUND; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php new file mode 100644 index 0000000000000..2a28945d98359 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php @@ -0,0 +1,199 @@ +getValue(Config::KEY_LOGIN_ID, $storeId); + } + + /** + * Gets the current environment + * + * @param int|null $storeId + * @return string + */ + public function getEnvironment($storeId = null): string + { + return $this->getValue(Config::KEY_ENVIRONMENT, $storeId); + } + + /** + * Gets the transaction key + * + * @param int|null $storeId + * @return string + */ + public function getTransactionKey($storeId = null): ?string + { + return $this->getValue(Config::KEY_TRANSACTION_KEY, $storeId); + } + + /** + * Gets the API endpoint URL + * + * @param int|null $storeId + * @return string + */ + public function getApiUrl($storeId = null): string + { + $environment = $this->getValue(Config::KEY_ENVIRONMENT, $storeId); + + return $environment === Environment::ENVIRONMENT_SANDBOX + ? self::ENDPOINT_URL_SANDBOX + : self::ENDPOINT_URL_PRODUCTION; + } + + /** + * Gets the configured signature key + * + * @param int|null $storeId + * @return string + */ + public function getTransactionSignatureKey($storeId = null): ?string + { + return $this->getValue(Config::KEY_SIGNATURE_KEY, $storeId); + } + + /** + * Gets the configured legacy transaction hash + * + * @param int|null $storeId + * @return string + */ + public function getLegacyTransactionHash($storeId = null): ?string + { + return $this->getValue(Config::KEY_LEGACY_TRANSACTION_HASH, $storeId); + } + + /** + * Gets the configured payment action + * + * @param int|null $storeId + * @return string + */ + public function getPaymentAction($storeId = null): ?string + { + return $this->getValue(Config::KEY_PAYMENT_ACTION, $storeId); + } + + /** + * Gets the configured client key + * + * @param int|null $storeId + * @return string + */ + public function getClientKey($storeId = null): ?string + { + return $this->getValue(Config::KEY_CLIENT_KEY, $storeId); + } + + /** + * Should authorize.net email the customer their receipt. + * + * @param int|null $storeId + * @return bool + */ + public function shouldEmailCustomer($storeId = null): bool + { + return (bool)$this->getValue(Config::KEY_SHOULD_EMAIL_CUSTOMER, $storeId); + } + + /** + * Should the cvv field be shown + * + * @param int|null $storeId + * @return bool + */ + public function isCvvEnabled($storeId = null): bool + { + return (bool)$this->getValue(Config::KEY_CVV_ENABLED, $storeId); + } + + /** + * Retrieves the solution id for the given store based on environment + * + * @param int|null $storeId + * @return string + */ + public function getSolutionId($storeId = null): ?string + { + $environment = $this->getValue(Config::KEY_ENVIRONMENT, $storeId); + + return $environment === Environment::ENVIRONMENT_SANDBOX + ? self::SOLUTION_ID_SANDBOX + : self::SOLUTION_ID_PRODUCTION; + } + + /** + * Returns the keys to be pulled from the transaction and displayed + * + * @param int|null $storeId + * @return string[] + */ + public function getAdditionalInfoKeys($storeId = null): array + { + return explode(',', $this->getValue(Config::KEY_ADDITIONAL_INFO_KEYS, $storeId) ?? ''); + } + + /** + * Returns the keys to be pulled from the transaction and displayed when syncing the transaction + * + * @param int|null $storeId + * @return string[] + */ + public function getTransactionInfoSyncKeys($storeId = null): array + { + return explode(',', $this->getValue(Config::KEY_TRANSACTION_SYNC_KEYS, $storeId) ?? ''); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php new file mode 100644 index 0000000000000..1b2efbb85721a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php @@ -0,0 +1,126 @@ +httpClientFactory = $httpClientFactory; + $this->config = $config; + $this->paymentLogger = $paymentLogger; + $this->logger = $logger; + $this->json = $json; + } + + /** + * Places request to gateway. Returns result as ENV array + * + * @param TransferInterface $transferObject + * @return array + * @throws \Magento\Payment\Gateway\Http\ClientException + */ + public function placeRequest(TransferInterface $transferObject) + { + $request = $transferObject->getBody(); + $log = [ + 'request' => $request, + ]; + $client = $this->httpClientFactory->create(); + $url = $this->config->getApiUrl(); + + $type = $request['payload_type']; + unset($request['payload_type']); + $request = [$type => $request]; + + try { + $client->setUri($url); + $client->setConfig(['maxredirects' => 0, 'timeout' => 30]); + $client->setRawData($this->json->serialize($request), 'application/json'); + $client->setMethod(ZendClient::POST); + + $responseBody = $client->request() + ->getBody(); + + // Strip BOM because Authorize.net sends it in the response + if ($responseBody && substr($responseBody, 0, 3) === pack('CCC', 0xef, 0xbb, 0xbf)) { + $responseBody = substr($responseBody, 3); + } + + $log['response'] = $responseBody; + + try { + $data = $this->json->unserialize($responseBody); + } catch (InvalidArgumentException $e) { + throw new \Exception('Invalid JSON was returned by the gateway'); + } + + return $data; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw new ClientException( + __('Something went wrong in the payment gateway.') + ); + } finally { + $this->paymentLogger->debug($log); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php new file mode 100644 index 0000000000000..a23397c09189a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php @@ -0,0 +1,42 @@ +fields = $fields; + } + + /** + * @inheritdoc + */ + public function filter(array $data): array + { + foreach ($this->fields as $field) { + unset($data[$field]); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php new file mode 100644 index 0000000000000..35e563eacb0cd --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php @@ -0,0 +1,23 @@ +transferBuilder = $transferBuilder; + $this->payloadFilters = $payloadFilters; + } + + /** + * Builds gateway transfer object + * + * @param array $request + * @return TransferInterface + */ + public function create(array $request) + { + foreach ($this->payloadFilters as $filter) { + $request = $filter->filter($request); + } + + return $this->transferBuilder + ->setBody($request) + ->build(); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php new file mode 100644 index 0000000000000..6883d63397be0 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php @@ -0,0 +1,65 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $authorizationTransaction = $payment->getAuthorizationTransaction(); + + if (empty($authorizationTransaction)) { + $transactionId = $payment->getLastTransId(); + } else { + $transactionId = $authorizationTransaction->getParentTxnId(); + + if (empty($transactionId)) { + $transactionId = $authorizationTransaction->getTxnId(); + } + } + + $data = [ + 'heldTransactionRequest' => [ + 'action' => 'approve', + 'refTransId' => $transactionId, + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php new file mode 100644 index 0000000000000..e9c42e864440c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php @@ -0,0 +1,77 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + $billingAddress = $order->getBillingAddress(); + $shippingAddress = $order->getShippingAddress(); + $result = [ + 'transactionRequest' => [] + ]; + + if ($billingAddress) { + $result['transactionRequest']['billTo'] = [ + 'firstName' => $billingAddress->getFirstname(), + 'lastName' => $billingAddress->getLastname(), + 'company' => $billingAddress->getCompany() ?? '', + 'address' => $billingAddress->getStreetLine1(), + 'city' => $billingAddress->getCity(), + 'state' => $billingAddress->getRegionCode(), + 'zip' => $billingAddress->getPostcode(), + 'country' => $billingAddress->getCountryId() + ]; + } + + if ($shippingAddress) { + $result['transactionRequest']['shipTo'] = [ + 'firstName' => $shippingAddress->getFirstname(), + 'lastName' => $shippingAddress->getLastname(), + 'company' => $shippingAddress->getCompany() ?? '', + 'address' => $shippingAddress->getStreetLine1(), + 'city' => $shippingAddress->getCity(), + 'state' => $shippingAddress->getRegionCode(), + 'zip' => $shippingAddress->getPostcode(), + 'country' => $shippingAddress->getCountryId() + ]; + } + + if ($order->getRemoteIp()) { + $result['transactionRequest']['customerIP'] = $order->getRemoteIp(); + } + + return $result; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php new file mode 100644 index 0000000000000..601c329fe4f76 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php @@ -0,0 +1,46 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + return [ + 'transactionRequest' => [ + 'amount' => $this->formatPrice($this->subjectReader->readAmount($buildSubject)), + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php new file mode 100644 index 0000000000000..2387ab0ab89f3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php @@ -0,0 +1,59 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * Adds authentication information to the request + * + * @param array $buildSubject + * @return array + */ + public function build(array $buildSubject): array + { + $storeId = $this->subjectReader->readStoreId($buildSubject); + + return [ + 'merchantAuthentication' => [ + 'name' => $this->config->getLoginId($storeId), + 'transactionKey' => $this->config->getTransactionKey($storeId) + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php new file mode 100644 index 0000000000000..226175f74d55a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php @@ -0,0 +1,69 @@ +subjectReader = $subjectReader; + $this->passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_AUTH_ONLY, + ] + ]; + + $this->passthroughData->setData( + 'transactionType', + $data['transactionRequest']['transactionType'] + ); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php new file mode 100644 index 0000000000000..0b17d10fb0d68 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php @@ -0,0 +1,73 @@ +subjectReader = $subjectReader; + $this->passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $authTransaction = $payment->getAuthorizationTransaction(); + $refId = $authTransaction->getAdditionalInformation('real_transaction_id'); + + $data = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_TYPE_PRIOR_AUTH_CAPTURE, + 'refTransId' => $refId + ] + ]; + + $this->passthroughData->setData( + 'transactionType', + $data['transactionRequest']['transactionType'] + ); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php new file mode 100644 index 0000000000000..e5b4472c098c8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php @@ -0,0 +1,62 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $result = []; + + if ($this->config->shouldEmailCustomer($this->subjectReader->readStoreId($buildSubject))) { + $result['transactionRequest'] = [ + 'transactionSettings' => [ + 'setting' => [ + [ + 'settingName' => 'emailCustomer', + 'settingValue' => 'true' + ] + ] + ] + ]; + } + + return $result; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php new file mode 100644 index 0000000000000..7cd0426e93dd7 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php @@ -0,0 +1,52 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + $billingAddress = $order->getBillingAddress(); + $result = [ + 'transactionRequest' => [ + 'customer' => [ + 'id' => $order->getCustomerId(), + 'email' => $billingAddress->getEmail() + ] + ] + ]; + + return $result; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php new file mode 100644 index 0000000000000..b0e33c9ca9615 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php @@ -0,0 +1,48 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + return [ + 'transactionRequest' => [ + 'order' => [ + 'invoiceNumber' => $order->getOrderIncrementId() + ] + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php new file mode 100644 index 0000000000000..0301d08ad42c5 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php @@ -0,0 +1,58 @@ +passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $fields = []; + + foreach ($this->passthroughData->getData() as $key => $value) { + $fields[] = [ + 'name' => $key, + 'value' => $value + ]; + } + + if (!empty($fields)) { + return [ + 'transactionRequest' => [ + 'userFields' => [ + 'userField' => $fields + ] + ] + ]; + } + + return []; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php new file mode 100644 index 0000000000000..1ad73f6236616 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php @@ -0,0 +1,56 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $dataDescriptor = $payment->getAdditionalInformation('opaqueDataDescriptor'); + $dataValue = $payment->getAdditionalInformation('opaqueDataValue'); + + $data['transactionRequest']['payment'] = [ + 'opaqueData' => [ + 'dataDescriptor' => $dataDescriptor, + 'dataValue' => $dataValue + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php new file mode 100644 index 0000000000000..ad8f8c2b05d91 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php @@ -0,0 +1,52 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'poNumber' => $payment->getPoNumber() + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php new file mode 100644 index 0000000000000..96f3e67720fea --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php @@ -0,0 +1,58 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + * @throws \Exception + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'payment' => [ + 'creditCard' => [ + 'cardNumber' => $payment->getAdditionalInformation('ccLast4'), + 'expirationDate' => 'XXXX' + ] + ] + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php new file mode 100644 index 0000000000000..b8cb5f858d05d --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php @@ -0,0 +1,53 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $transactionId = $payment->getAuthorizationTransaction()->getParentTxnId(); + $data = [ + 'transactionRequest' => [ + 'refTransId' => $transactionId + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php new file mode 100644 index 0000000000000..752be05f6b576 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php @@ -0,0 +1,31 @@ + [ + 'transactionType' => self::REQUEST_TYPE_REFUND + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php new file mode 100644 index 0000000000000..16c3f9556de27 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php @@ -0,0 +1,45 @@ +type = $type; + } + + /** + * Adds the type of the request to the build subject + * + * @param array $buildSubject + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(array $buildSubject): array + { + return [ + 'payload_type' => $this->type + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php new file mode 100644 index 0000000000000..6ec27b105615b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php @@ -0,0 +1,69 @@ +subjectReader = $subjectReader; + $this->passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_AUTH_AND_CAPTURE, + ] + ]; + + $this->passthroughData->setData( + 'transactionType', + $data['transactionRequest']['transactionType'] + ); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php new file mode 100644 index 0000000000000..390714579f0b3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php @@ -0,0 +1,56 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + $data = []; + + if ($payment instanceof Payment && $order instanceof Order) { + $data = [ + 'transactionRequest' => [ + 'shipping' => [ + 'amount' => $order->getBaseShippingAmount() + ] + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php new file mode 100644 index 0000000000000..0c89a0116defe --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php @@ -0,0 +1,53 @@ +config = $config; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + return [ + 'transactionRequest' => [ + 'solution' => [ + 'id' => $this->config->getSolutionId($this->subjectReader->readStoreId($buildSubject)), + ] + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php new file mode 100644 index 0000000000000..f44b1e5de9a28 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php @@ -0,0 +1,43 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + return [ + 'store_id' => $order->getStoreId() + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php new file mode 100644 index 0000000000000..e3a17e9636846 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php @@ -0,0 +1,69 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $data = []; + + if (!empty($buildSubject['transactionId'])) { + $data = [ + 'transId' => $buildSubject['transactionId'] + ]; + } else { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $authorizationTransaction = $payment->getAuthorizationTransaction(); + + if (empty($authorizationTransaction)) { + $transactionId = $payment->getLastTransId(); + } else { + $transactionId = $authorizationTransaction->getParentTxnId(); + + if (empty($transactionId)) { + $transactionId = $authorizationTransaction->getTxnId(); + } + } + + $data = [ + 'transId' => $transactionId + ]; + } + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php new file mode 100644 index 0000000000000..ef0cb96774e62 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php @@ -0,0 +1,60 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $transactionData = []; + + if ($payment instanceof Payment) { + $authorizationTransaction = $payment->getAuthorizationTransaction(); + $refId = $authorizationTransaction->getAdditionalInformation('real_transaction_id'); + if (empty($refId)) { + $refId = $authorizationTransaction->getParentTxnId(); + } + + $transactionData['transactionRequest'] = [ + 'transactionType' => self::REQUEST_TYPE_VOID, + 'refTransId' => $refId + ]; + } + + return $transactionData; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php new file mode 100644 index 0000000000000..30b1ce88b083a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php @@ -0,0 +1,45 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $payment->setShouldCloseParentTransaction(true); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php new file mode 100644 index 0000000000000..f0dff200e802b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php @@ -0,0 +1,46 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $payment->setIsTransactionClosed(true); + $payment->setShouldCloseParentTransaction(true); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php new file mode 100644 index 0000000000000..16e8fbabb214a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php @@ -0,0 +1,55 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionResponse = $response['transactionResponse']; + + if ($payment instanceof Payment) { + $payment->setCcLast4($payment->getAdditionalInformation('ccLast4')); + $payment->setCcAvsStatus($transactionResponse['avsResultCode']); + $payment->setIsTransactionClosed(false); + + if ($transactionResponse['responseCode'] == self::RESPONSE_CODE_HELD) { + $payment->setIsTransactionPending(true) + ->setIsFraudDetected(true); + } + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php new file mode 100644 index 0000000000000..9f7c62873669f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php @@ -0,0 +1,63 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + $status = $response['transaction']['transactionStatus']; + // This data is only used when updating the order payment via Get Payment Update + if (!in_array($status, self::REVIEW_PENDING_STATUSES)) { + $denied = in_array($status, self::REVIEW_DECLINED_STATUSES); + $payment->setData('is_transaction_denied', $denied); + $payment->setData('is_transaction_approved', !$denied); + } + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php new file mode 100644 index 0000000000000..0dab641452136 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php @@ -0,0 +1,65 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $storeId = $this->subjectReader->readStoreId($handlingSubject); + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionResponse = $response['transactionResponse']; + + if ($payment instanceof Payment) { + // Add the keys that should show in the transaction details interface + $additionalInformationKeys = $this->config->getAdditionalInfoKeys($storeId); + $rawDetails = []; + foreach ($additionalInformationKeys as $paymentInfoKey) { + if (isset($transactionResponse[$paymentInfoKey])) { + $rawDetails[$paymentInfoKey] = $transactionResponse[$paymentInfoKey]; + $payment->setAdditionalInformation($paymentInfoKey, $transactionResponse[$paymentInfoKey]); + } + } + $payment->setTransactionAdditionalInfo(Payment\Transaction::RAW_DETAILS, $rawDetails); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php new file mode 100644 index 0000000000000..bf5257f95dad6 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php @@ -0,0 +1,54 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionResponse = $response['transactionResponse']; + + if ($payment instanceof Payment) { + if (!$payment->getParentTransactionId() + || $transactionResponse['transId'] != $payment->getParentTransactionId() + ) { + $payment->setTransactionId($transactionResponse['transId']); + } + $payment->setTransactionAdditionalInfo( + 'real_transaction_id', + $transactionResponse['transId'] + ); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php new file mode 100644 index 0000000000000..06b16b37278ba --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php @@ -0,0 +1,49 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionId = $response['transactionResponse']['transId']; + + if ($payment instanceof Payment) { + $payment->setIsTransactionClosed(true); + $payment->setShouldCloseParentTransaction(true); + $payment->setTransactionAdditionalInfo('real_transaction_id', $transactionId); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php new file mode 100644 index 0000000000000..855d48e27968e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php @@ -0,0 +1,96 @@ +readPayment($subject) + ->getOrder() + ->getStoreId(); + } catch (\InvalidArgumentException $e) { + // No store id is current set + } + } + + return $storeId ? (int)$storeId : null; + } + + /** + * Reads amount from subject + * + * @param array $subject + * @return string + */ + public function readAmount(array $subject): string + { + return (string)Helper\SubjectReader::readAmount($subject); + } + + /** + * Reads response from subject + * + * @param array $subject + * @return array + */ + public function readResponse(array $subject): ?array + { + return Helper\SubjectReader::readResponse($subject); + } + + /** + * Reads login id from subject + * + * @param array $subject + * @return string|null + */ + public function readLoginId(array $subject): ?string + { + return $subject['merchantAuthentication']['name'] ?? null; + } + + /** + * Reads transaction key from subject + * + * @param array $subject + * @return string|null + */ + public function readTransactionKey(array $subject): ?string + { + return $subject['merchantAuthentication']['transactionKey'] ?? null; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php new file mode 100644 index 0000000000000..7ad4647b421a1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php @@ -0,0 +1,79 @@ +resultFactory = $resultFactory; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject): ResultInterface + { + $response = $this->subjectReader->readResponse($validationSubject); + $isValid = (isset($response['messages']['resultCode']) + && $response['messages']['resultCode'] === self::RESULT_CODE_SUCCESS); + $errorCodes = []; + $errorMessages = []; + + if (!$isValid) { + if (isset($response['messages']['message']['code'])) { + $errorCodes[] = $response['messages']['message']['code']; + $errorMessages[] = $response['messages']['message']['text']; + } elseif (isset($response['messages']['message'])) { + foreach ($response['messages']['message'] as $message) { + $errorCodes[] = $message['code']; + $errorMessages[] = $message['text']; + } + } elseif (isset($response['errors']['error'])) { + foreach ($response['errors']['error'] as $message) { + $errorCodes[] = $message['errorCode']; + $errorMessages[] = $message['errorText']; + } + } + } + + return $this->createResult($isValid, $errorMessages, $errorCodes); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php new file mode 100644 index 0000000000000..0d1c2ad033d87 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php @@ -0,0 +1,197 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * Validates the transaction hash matches the configured hash + * + * @param array $validationSubject + * @return ResultInterface + */ + public function validate(array $validationSubject): ResultInterface + { + $response = $this->subjectReader->readResponse($validationSubject); + $storeId = $this->subjectReader->readStoreId($validationSubject); + + if (!empty($response['transactionResponse']['transHashSha2'])) { + return $this->validateHash( + $validationSubject, + $this->config->getTransactionSignatureKey($storeId), + 'transHashSha2', + 'generateSha512Hash' + ); + } elseif (!empty($response['transactionResponse']['transHash'])) { + return $this->validateHash( + $validationSubject, + $this->config->getLegacyTransactionHash($storeId), + 'transHash', + 'generateMd5Hash' + ); + } + + return $this->createResult( + false, + [ + __('The authenticity of the gateway response could not be verified.') + ], + [self::ERROR_TRANSACTION_HASH] + ); + } + + /** + * Validates the response again the legacy MD5 spec + * + * @param array $validationSubject + * @param string $storedHash + * @param string $hashField + * @param string $generateFunction + * @return ResultInterface + */ + private function validateHash( + array $validationSubject, + string $storedHash, + string $hashField, + string $generateFunction + ): ResultInterface { + $storeId = $this->subjectReader->readStoreId($validationSubject); + $response = $this->subjectReader->readResponse($validationSubject); + $transactionResponse = $response['transactionResponse']; + + /* + * Authorize.net is inconsistent with how they hash and heuristically trying to detect whether or not they used + * the amount to calculate the hash is risky because their responses are incorrect in some cases. + * Refund uses the amount when referencing a transaction but will use 0 when refunding without a reference. + * Non-refund reference transactions such as (void/capture) don't use the amount. Authorize/auth&capture + * transactions will use amount but if there is an AVS error the response will indicate the transaction was a + * reference transaction so this can't be heuristically detected by looking at combinations of refTransID + * and transId (yes they also mixed the letter casing for "id"). Their documentation doesn't talk about this + * and to make this even better, none of their official SDKs support the new hash field to compare + * implementations. Therefore the only way to safely validate this hash without failing for even more + * unexpected corner cases we simply need to validate with and without the amount. + */ + try { + $amount = $this->subjectReader->readAmount($validationSubject); + } catch (\InvalidArgumentException $e) { + $amount = 0; + } + + $hash = $this->{$generateFunction}( + $storedHash, + $this->config->getLoginId($storeId), + sprintf('%.2F', $amount), + $transactionResponse['transId'] ?? '' + ); + $valid = Security::compareStrings($hash, $transactionResponse[$hashField]); + + if (!$valid && $amount > 0) { + $hash = $this->{$generateFunction}( + $storedHash, + $this->config->getLoginId($storeId), + '0.00', + $transactionResponse['transId'] ?? '' + ); + $valid = Security::compareStrings($hash, $transactionResponse[$hashField]); + } + + if ($valid) { + return $this->createResult(true); + } + + return $this->createResult( + false, + [ + __('The authenticity of the gateway response could not be verified.') + ], + [self::ERROR_TRANSACTION_HASH] + ); + } + + /** + * Generates a Md5 hash to compare against AuthNet's. + * + * @param string $merchantMd5 + * @param string $merchantApiLogin + * @param string $amount + * @param string $transactionId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function generateMd5Hash( + $merchantMd5, + $merchantApiLogin, + $amount, + $transactionId + ) { + return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); + } + + /** + * Generates a SHA-512 hash to compare against AuthNet's. + * + * @param string $merchantKey + * @param string $merchantApiLogin + * @param string $amount + * @param string $transactionId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function generateSha512Hash( + $merchantKey, + $merchantApiLogin, + $amount, + $transactionId + ) { + $message = '^' . $merchantApiLogin . '^' . $transactionId . '^' . $amount . '^'; + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantKey))); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php new file mode 100644 index 0000000000000..326f4fb29ac84 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php @@ -0,0 +1,101 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject): ResultInterface + { + $response = $this->subjectReader->readResponse($validationSubject); + $transactionResponse = $response['transactionResponse']; + + if ($this->isResponseCodeAnError($transactionResponse)) { + $errorCodes = []; + $errorMessages = []; + + if (isset($transactionResponse['messages']['message']['code'])) { + $errorCodes[] = $transactionResponse['messages']['message']['code']; + $errorMessages[] = $transactionResponse['messages']['message']['text']; + } elseif (isset($transactionResponse['messages']['message'])) { + foreach ($transactionResponse['messages']['message'] as $message) { + $errorCodes[] = $message['code']; + $errorMessages[] = $message['description']; + } + } elseif (isset($transactionResponse['errors'])) { + foreach ($transactionResponse['errors'] as $message) { + $errorCodes[] = $message['errorCode']; + $errorMessages[] = $message['errorText']; + } + } + + return $this->createResult(false, $errorMessages, $errorCodes); + } + + return $this->createResult(true); + } + + /** + * Determines if the response code is actually an error + * + * @param array $transactionResponse + * @return bool + */ + private function isResponseCodeAnError(array $transactionResponse): bool + { + $code = $transactionResponse['messages']['message']['code'] + ?? $transactionResponse['messages']['message'][0]['code'] + ?? $transactionResponse['errors'][0]['errorCode'] + ?? null; + + return !in_array($transactionResponse['responseCode'], [ + self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_HELD + ]) + || $code + && !in_array( + $code, + [ + self::RESPONSE_REASON_CODE_APPROVED, + self::RESPONSE_REASON_CODE_PENDING_REVIEW, + self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED + ] + ); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/LICENSE.txt b/app/code/Magento/AuthorizenetAcceptjs/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetAcceptjs/LICENSE_AFL.txt b/app/code/Magento/AuthorizenetAcceptjs/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php new file mode 100644 index 0000000000000..046907ebb88cc --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php @@ -0,0 +1,25 @@ + self::ENVIRONMENT_SANDBOX, + 'label' => 'Sandbox', + ], + [ + 'value' => self::ENVIRONMENT_PRODUCTION, + 'label' => 'Production' + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php new file mode 100644 index 0000000000000..907a1b2a51b85 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php @@ -0,0 +1,32 @@ + 'authorize', + 'label' => __('Authorize Only'), + ], + [ + 'value' => 'authorize_capture', + 'label' => __('Authorize and Capture') + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php b/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php new file mode 100644 index 0000000000000..b49ef7e622506 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php @@ -0,0 +1,19 @@ +config = $config; + $this->cart = $cart; + } + + /** + * Retrieve assoc array of checkout configuration + * + * @return array + */ + public function getConfig() + { + $storeId = $this->cart->getStoreId(); + + return [ + 'payment' => [ + Config::METHOD => [ + 'clientKey' => $this->config->getClientKey($storeId), + 'apiLoginID' => $this->config->getLoginId($storeId), + 'environment' => $this->config->getEnvironment($storeId), + 'useCvv' => $this->config->isCvvEnabled($storeId), + ] + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php b/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php new file mode 100644 index 0000000000000..c7490ad0c80c3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php @@ -0,0 +1,52 @@ +readDataArgument($observer); + + $additionalData = $data->getData(PaymentInterface::KEY_ADDITIONAL_DATA); + if (!is_array($additionalData)) { + return; + } + + $paymentInfo = $this->readPaymentModelArgument($observer); + + foreach ($this->additionalInformationList as $additionalInformationKey) { + if (isset($additionalData[$additionalInformationKey])) { + $paymentInfo->setAdditionalInformation( + $additionalInformationKey, + $additionalData[$additionalInformationKey] + ); + } + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/README.md b/app/code/Magento/AuthorizenetAcceptjs/README.md new file mode 100644 index 0000000000000..b066f8a2d7509 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/README.md @@ -0,0 +1 @@ +The Magento_AuthorizenetAcceptjs module implements the integration with the Authorize.Net payment gateway and makes the latter available as a payment method in Magento. diff --git a/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php b/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php new file mode 100644 index 0000000000000..0675bd94b6200 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php @@ -0,0 +1,230 @@ +scopeConfig = $scopeConfig; + $this->resourceConfig = $resourceConfig; + $this->encryptor = $encryptor; + $this->moduleDataSetup = $moduleDataSetup; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function apply(): void + { + $this->moduleDataSetup->startSetup(); + $this->migrateDefaultValues(); + $this->migrateWebsiteValues(); + $this->moduleDataSetup->endSetup(); + } + + /** + * Migrate configuration values from DirectPost to Accept.js on default scope + * + * @return void + */ + private function migrateDefaultValues(): void + { + foreach ($this->configFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field); + + if (!empty($configValue)) { + $this->saveNewConfigValue($field, $configValue); + } + } + + foreach ($this->encryptedConfigFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field); + + if (!empty($configValue)) { + $this->saveNewConfigValue( + $field, + $configValue, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0, + true + ); + } + } + } + + /** + * Migrate configuration values from DirectPost to Accept.js on all website scopes + * + * @return void + */ + private function migrateWebsiteValues(): void + { + foreach ($this->storeManager->getWebsites() as $website) { + $websiteID = (int) $website->getId(); + + foreach ($this->configFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field, ScopeInterface::SCOPE_WEBSITES, $websiteID); + + if (!empty($configValue)) { + $this->saveNewConfigValue($field, $configValue, ScopeInterface::SCOPE_WEBSITES, $websiteID); + } + } + + foreach ($this->encryptedConfigFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field, ScopeInterface::SCOPE_WEBSITES, $websiteID); + + if (!empty($configValue)) { + $this->saveNewConfigValue($field, $configValue, ScopeInterface::SCOPE_WEBSITES, $websiteID, true); + } + } + } + } + + /** + * Get old configuration value from the DirectPost module's configuration on the store scope + * + * @param string $field + * @param string $scope + * @param int $scopeID + * @return mixed + */ + private function getOldConfigValue( + string $field, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + int $scopeID = null + ) { + return $this->scopeConfig->getValue( + sprintf(self::PAYMENT_PATH_FORMAT, self::DIRECTPOST_PATH, $field), + $scope, + $scopeID + ); + } + + /** + * Save configuration value for AcceptJS + * + * @param string $field + * @param mixed $value + * @param string $scope + * @param int $scopeID + * @param bool $isEncrypted + * @return void + */ + private function saveNewConfigValue( + string $field, + $value, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + int $scopeID = 0, + bool $isEncrypted = false + ): void { + $value = $isEncrypted ? $this->encryptor->encrypt($value) : $value; + + $this->resourceConfig->saveConfig( + sprintf(self::PAYMENT_PATH_FORMAT, self::ACCEPTJS_PATH, $field), + $value, + $scope, + $scopeID + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml new file mode 100644 index 0000000000000..e9a194435e3eb --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml new file mode 100644 index 0000000000000..d06bf996a1f25 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml new file mode 100644 index 0000000000000..ba0e49d1876f0 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml new file mode 100644 index 0000000000000..59d4be98d450c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml @@ -0,0 +1,18 @@ + + + + + + $24.68 + $128.00 + Processing + Capture + No + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE.txt b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/README.md b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/README.md new file mode 100644 index 0000000000000..aba235e2cfad9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# AuthorizenetAcceptjs Functional Tests + +The Functional Test Module for **Magento AuthorizenetAcceptjs** module. diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml new file mode 100644 index 0000000000000..defb91339ea8f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml @@ -0,0 +1,25 @@ + + + + +
+ + + + + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml new file mode 100644 index 0000000000000..31be865ea2678 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml @@ -0,0 +1,24 @@ + + + + +
+ + + + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml new file mode 100644 index 0000000000000..5d97842de374c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml new file mode 100644 index 0000000000000..344330c4bc052 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml new file mode 100644 index 0000000000000..b5f2ecf641162 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml new file mode 100644 index 0000000000000..7ae3dd0ffee89 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml new file mode 100644 index 0000000000000..f9f1bef38d17d --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml new file mode 100644 index 0000000000000..e54f9808fd49e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml new file mode 100644 index 0000000000000..608067d7d31a1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml @@ -0,0 +1,25 @@ + + + + +
+ + + + + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml new file mode 100644 index 0000000000000..42a78291436ed --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml @@ -0,0 +1,74 @@ + + + + + + + + + <description value="Capture an order placed using Authorize.net Accept.js"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12255"/> + <skip> + <issueId value="DEVOPS-4604"/> + </skip> + <group value="AuthorizenetAcceptjs"/> + <group value="ThirdPartyPayments"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <createData stepKey="createCustomer" entity="Simple_US_Customer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Configure Auth.net--> + <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> + <argument name="paymentAction" value="Authorize Only"/> + </actionGroup> + + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="DisableAuthorizenetAcceptjs" stepKey="DisableAuthorizenetAcceptjs"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Storefront Login--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginStorefront"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Add product to cart--> + <amOnPage url="$$createProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForCartToFill"/> + + <!--Checkout steps--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + <waitForPageLoad stepKey="waitForCheckoutLoad"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="submitShippingSelection"/> + <waitForPageLoad stepKey="waitForShippingToFinish"/> + <actionGroup ref="FillPaymentInformation" stepKey="fillPaymentInfo"/> + + <!--View and validate order--> + <actionGroup ref="ViewAndValidateOrderActionGroup" stepKey="viewAndValidateOrder"> + <argument name="amount" value="{{AuthorizenetAcceptjsOrderValidationData.twoSimpleProductsOrderAmount}}"/> + <argument name="status" value="{{AuthorizenetAcceptjsOrderValidationData.processingStatusProcessing}}"/> + <argument name="captureStatus" value="{{AuthorizenetAcceptjsOrderValidationData.captureStatusCapture}}"/> + <argument name="closedStatus" value="{{AuthorizenetAcceptjsOrderValidationData.closedStatusNo}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml new file mode 100644 index 0000000000000..95c2436905212 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml @@ -0,0 +1,82 @@ +<?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="GuestCheckoutVirtualProductAuthorizenetAcceptjsTest"> + <annotations> + <stories value="Authorize.net Accept.js"/> + <title value="Guest Checkout of Virtual Product using Authorize.net Accept.js"/> + <description value="Checkout a virtual product with a guest using Authorize.net Accept.js"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12712"/> + <skip> + <issueId value="DEVOPS-4604"/> + </skip> + <group value="AuthorizenetAcceptjs"/> + <group value="ThirdPartyPayments"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create virtual product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="defaultVirtualProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> + <argument name="product" value="defaultVirtualProduct"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Configure Auth.net--> + <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> + <argument name="paymentAction" value="Authorize and Capture"/> + </actionGroup> + + </before> + <after> + <actionGroup ref="DisableAuthorizenetAcceptjs" stepKey="DisableAuthorizenetAcceptjs"/> + <!-- Delete virtual product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="defaultVirtualProduct"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Add product to cart twice--> + <amOnPage url="{{defaultVirtualProduct.sku}}.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForCartToFill"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCartAgain"/> + <waitForPageLoad stepKey="waitForCartToFillAgain"/> + + <!--Checkout steps--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + <waitForPageLoad stepKey="waitForCheckoutLoad"/> + + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="enterEmail"/> + <click stepKey="clickOnAuthorizenetToggle" selector="{{AuthorizenetCheckoutSection.selectAuthorizenet}}"/> + <waitForPageLoad stepKey="waitForBillingInfoLoad"/> + <actionGroup ref="GuestCheckoutAuthorizenetFillBillingAddress" stepKey="fillAddressForm"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="FillPaymentInformation" stepKey="fillPaymentInfo"/> + + <!--View and validate order--> + <actionGroup ref="ViewAndValidateOrderActionGroupNoSubmit" stepKey="viewAndValidateOrder"> + <argument name="amount" value="{{AuthorizenetAcceptjsOrderValidationData.virtualProductOrderAmount}}"/> + <argument name="status" value="{{AuthorizenetAcceptjsOrderValidationData.processingStatusProcessing}}"/> + <argument name="captureStatus" value="{{AuthorizenetAcceptjsOrderValidationData.captureStatusCapture}}"/> + <argument name="closedStatus" value="{{AuthorizenetAcceptjsOrderValidationData.closedStatusNo}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/FormTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/FormTest.php new file mode 100644 index 0000000000000..020b651aaaf17 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/FormTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Block; + +use Magento\AuthorizenetAcceptjs\Block\Form; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\View\Element\Template\Context; +use Magento\Payment\Model\Config as PaymentConfig; + +class FormTest extends TestCase +{ + /** + * @var Form + */ + private $block; + + /** + * @var Config|MockObject|InvocationMocker + */ + private $configMock; + + protected function setUp() + { + $contextMock = $this->createMock(Context::class); + $this->configMock = $this->createMock(Config::class); + $quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods(['getStoreId']) + ->getMock(); + $quoteMock->method('getStoreId') + ->willReturn('123'); + $paymentConfig = $this->createMock(PaymentConfig::class); + + $this->block = new Form( + $contextMock, + $paymentConfig, + $this->configMock, + $quoteMock + ); + } + + public function testIsCvvEnabled() + { + $this->configMock->method('isCvvEnabled') + ->with('123') + ->willReturn(true); + $this->assertTrue($this->block->isCvvEnabled()); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/InfoTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/InfoTest.php new file mode 100644 index 0000000000000..70dfb140e1576 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/InfoTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Block; + +use Magento\AuthorizenetAcceptjs\Block\Info; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Payment\Gateway\ConfigInterface; +use Magento\Payment\Model\InfoInterface; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class InfoTest extends TestCase +{ + public function testLabelsAreTranslated() + { + /** @var Context|MockObject|InvocationMocker $contextMock */ + $contextMock = $this->createMock(Context::class); + /** @var Config|MockObject|InvocationMocker $configMock */ + $configMock = $this->createMock(ConfigInterface::class); + $block = new Info($contextMock, $configMock); + /** @var InfoInterface|MockObject|InvocationMocker $payment */ + $payment = $this->createMock(InfoInterface::class); + /** @var RendererInterface|MockObject|InvocationMocker $translationRenderer */ + $translationRenderer = $this->createMock(RendererInterface::class); + + // only foo should be used + $configMock->method('getValue') + ->willReturnMap([ + ['paymentInfoKeys', null, 'foo'], + ['privateInfoKeys', null, ''] + ]); + + // Give more info to ensure only foo is translated + $payment->method('getAdditionalInformation') + ->willReturnCallback(function ($name = null) { + $info = [ + 'foo' => 'bar', + 'baz' => 'bash' + ]; + + if (empty($name)) { + return $info; + } + + return $info[$name]; + }); + + // Foo should be translated to Super Cool String + $translationRenderer->method('render') + ->with(['foo'], []) + ->willReturn('Super Cool String'); + + $previousRenderer = Phrase::getRenderer(); + Phrase::setRenderer($translationRenderer); + + try { + $block->setData('info', $payment); + + $info = $block->getSpecificInformation(); + } finally { + // No matter what, restore the renderer + Phrase::setRenderer($previousRenderer); + } + + // Assert the label was correctly translated + $this->assertSame($info['Super Cool String'], 'bar'); + $this->assertArrayNotHasKey('foo', $info); + $this->assertArrayNotHasKey('baz', $info); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/PaymentTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/PaymentTest.php new file mode 100644 index 0000000000000..11ae27f9d2ea7 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/PaymentTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Block; + +use Magento\AuthorizenetAcceptjs\Block\Payment; +use Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\Element\Template\Context; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentTest extends TestCase +{ + /** + * @var ConfigProvider|MockObject|InvocationMocker + */ + private $configMock; + + /** + * @var Payment + */ + private $block; + + protected function setUp() + { + $contextMock = $this->createMock(Context::class); + $this->configMock = $this->createMock(ConfigProvider::class); + $this->block = new Payment($contextMock, $this->configMock, new Json()); + } + + public function testConfigIsCreated() + { + $this->configMock->method('getConfig') + ->willReturn([ + 'payment' => [ + 'authorizenet_acceptjs' => [ + 'foo' => 'bar' + ] + ] + ]); + + $result = $this->block->getPaymentConfig(); + $expected = '{"foo":"bar","code":"authorizenet_acceptjs"}'; + $this->assertEquals($expected, $result); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/AcceptPaymentStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/AcceptPaymentStrategyCommandTest.php new file mode 100644 index 0000000000000..316fef5443360 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/AcceptPaymentStrategyCommandTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\AcceptPaymentStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\Command\RefundTransactionStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AcceptPaymentStrategyCommandTest extends TestCase +{ + /** + * @var CommandInterface|MockObject + */ + private $commandMock; + + /** + * @var CommandInterface|MockObject + */ + private $transactionDetailsCommandMock; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var RefundTransactionStrategyCommand + */ + private $command; + + /** + * @var ResultInterface|MockObject + */ + private $transactionResultMock; + + protected function setUp() + { + $this->transactionDetailsCommandMock = $this->createMock(CommandInterface::class); + $this->commandMock = $this->createMock(CommandInterface::class); + $this->transactionResultMock = $this->createMock(ResultInterface::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->command = new AcceptPaymentStrategyCommand( + $this->commandPoolMock, + new SubjectReader() + ); + } + + /** + * @param string $status + * @dataProvider inReviewStatusesProvider + */ + public function testCommandWillAcceptInTheGatewayWhenInFDSReview(string $status) + { + // Assert command is executed + $this->commandMock->expects($this->once()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ['accept_fds', $this->commandMock] + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => $status + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + public function testCommandWillDoNothingWhenTransactionHasAlreadyBeenAuthorized() + { + // Assert command is never executed + $this->commandMock->expects($this->never()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'anythingelseisfine' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + public function inReviewStatusesProvider() + { + return [ + ['FDSPendingReview'], + ['FDSAuthorizedPendingReview'] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php new file mode 100644 index 0000000000000..4cbded9764793 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php @@ -0,0 +1,181 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\CaptureStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\GatewayCommand; +use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Sales\Api\Data\TransactionSearchResultInterface; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CaptureStrategyCommandTest extends TestCase +{ + /** + * @var CaptureStrategyCommand + */ + private $strategyCommand; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var TransactionRepositoryInterface|MockObject + */ + private $transactionRepositoryMock; + + /** + * @var FilterBuilder|MockObject + */ + private $filterBuilderMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObject|MockObject + */ + private $paymentDOMock; + + /** + * @var GatewayCommand|MockObject + */ + private $commandMock; + + /** + * @var TransactionSearchResultInterface|MockObject + */ + private $transactionsResult; + + protected function setUp() + { + // Simple mocks + $this->paymentDOMock = $this->createMock(PaymentDataObject::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->commandMock = $this->createMock(GatewayCommand::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $this->transactionRepositoryMock = $this->createMock(TransactionRepositoryInterface::class); + + // The search criteria builder should return the criteria with the specified filters + $this->filterBuilderMock = $this->createMock(FilterBuilder::class); + // We aren't coupling the implementation to the test. The test only cares how the result is processed + $this->filterBuilderMock->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->method('setValue') + ->willReturnSelf(); + $searchCriteria = new SearchCriteria(); + $this->searchCriteriaBuilderMock->method('addFilters') + ->willReturnSelf(); + $this->searchCriteriaBuilderMock->method('create') + ->willReturn($searchCriteria); + // The transaction result can be customized per test to simulate different scenarios + $this->transactionsResult = $this->createMock(TransactionSearchResultInterface::class); + $this->transactionRepositoryMock->method('getList') + ->with($searchCriteria) + ->willReturn($this->transactionsResult); + + $this->strategyCommand = new CaptureStrategyCommand( + $this->commandPoolMock, + $this->transactionRepositoryMock, + $this->filterBuilderMock, + $this->searchCriteriaBuilderMock, + new SubjectReader() + ); + } + + public function testExecuteWillAuthorizeWhenNotAuthorizedAndNotCaptured() + { + $subject = ['payment' => $this->paymentDOMock]; + + // Hasn't been authorized + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn(false); + // Hasn't been captured + $this->transactionsResult->method('getTotalCount') + ->willReturn(0); + // Assert authorize command was used + $this->commandPoolMock->expects($this->once()) + ->method('get') + ->with('sale') + ->willReturn($this->commandMock); + // Assert execute was called and with correct data + $this->commandMock->expects($this->once()) + ->method('execute') + ->with($subject); + + $this->strategyCommand->execute($subject); + // Assertions are performed via mock expects above + } + + public function testExecuteWillAuthorizeAndCaptureWhenAlreadyCaptured() + { + $subject = ['payment' => $this->paymentDOMock]; + + // Already authorized + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn(true); + // And already captured + $this->transactionsResult->method('getTotalCount') + ->willReturn(1); + // Assert authorize command was used + $this->commandPoolMock->expects($this->once()) + ->method('get') + ->with('settle') + ->willReturn($this->commandMock); + // Assert execute was called and with correct data + $this->commandMock->expects($this->once()) + ->method('execute') + ->with($subject); + + $this->strategyCommand->execute($subject); + // Assertions are performed via mock expects above + } + + public function testExecuteWillCaptureWhenAlreadyAuthorizedButNotCaptured() + { + $subject = ['payment' => $this->paymentDOMock]; + + // Was already authorized + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn(true); + // But, hasn't been captured + $this->transactionsResult->method('getTotalCount') + ->willReturn(0); + // Assert authorize command was used + $this->commandPoolMock->expects($this->once()) + ->method('get') + ->with('settle') + ->willReturn($this->commandMock); + // Assert execute was called and with correct data + $this->commandMock->expects($this->once()) + ->method('execute') + ->with($subject); + + $this->strategyCommand->execute($subject); + // Assertions are performed via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/FetchTransactionInfoCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/FetchTransactionInfoCommandTest.php new file mode 100644 index 0000000000000..757500c7e50eb --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/FetchTransactionInfoCommandTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\FetchTransactionInfoCommand; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class FetchTransactionInfoCommandTest extends TestCase +{ + /** + * @var CommandInterface|MockObject + */ + private $transactionDetailsCommandMock; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var FetchTransactionInfoCommand + */ + private $command; + + /** + * @var ResultInterface|MockObject + */ + private $transactionResultMock; + + /** + * @var PaymentDataObject|MockObject + */ + private $paymentDOMock; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Config + */ + private $configMock; + + /** + * @var HandlerInterface + */ + private $handlerMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObject::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->configMock = $this->createMock(Config::class); + $this->configMock->method('getTransactionInfoSyncKeys') + ->willReturn(['foo', 'bar']); + $orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($orderMock); + $this->transactionDetailsCommandMock = $this->createMock(CommandInterface::class); + $this->transactionResultMock = $this->createMock(ResultInterface::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->handlerMock = $this->createMock(HandlerInterface::class); + $this->command = new FetchTransactionInfoCommand( + $this->commandPoolMock, + new SubjectReader(), + $this->configMock, + $this->handlerMock + ); + } + + public function testCommandWillMarkTransactionAsApprovedWhenNotVoid() + { + $response = [ + 'transaction' => [ + 'transactionStatus' => 'authorizedPendingCapture', + 'foo' => 'abc', + 'bar' => 'cba', + 'dontreturnme' => 'justdont' + ] + ]; + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ]); + + $this->transactionResultMock->method('get') + ->willReturn($response); + + $buildSubject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->handlerMock->expects($this->once()) + ->method('handle') + ->with($buildSubject, $response) + ->willReturn($this->transactionResultMock); + + $result = $this->command->execute($buildSubject); + + $expected = [ + 'foo' => 'abc', + 'bar' => 'cba' + ]; + + $this->assertSame($expected, $result); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/GatewayQueryCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/GatewayQueryCommandTest.php new file mode 100644 index 0000000000000..e37db34936385 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/GatewayQueryCommandTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\GatewayQueryCommand; +use Magento\Payment\Gateway\Command\Result\ArrayResult; +use Magento\Payment\Gateway\Http\ClientInterface; +use Magento\Payment\Gateway\Http\TransferFactoryInterface; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Payment\Gateway\Validator\Result; +use Magento\Payment\Gateway\Validator\ValidatorInterface; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class GatewayQueryCommandTest extends TestCase +{ + /** + * @var GatewayQueryCommand + */ + private $command; + + /** + * @var BuilderInterface|MockObject|InvocationMocker + */ + private $requestBuilderMock; + + /** + * @var TransferFactoryInterface|MockObject|InvocationMocker + */ + private $transferFactoryMock; + + /** + * @var ClientInterface|MockObject|InvocationMocker + */ + private $clientMock; + + /** + * @var LoggerInterface|MockObject|InvocationMocker + */ + private $loggerMock; + + /** + * @var ValidatorInterface|MockObject|InvocationMocker + */ + private $validatorMock; + + /** + * @var TransferInterface|MockObject|InvocationMocker + */ + private $transferMock; + + protected function setUp() + { + $this->requestBuilderMock = $this->createMock(BuilderInterface::class); + $this->transferFactoryMock = $this->createMock(TransferFactoryInterface::class); + $this->transferMock = $this->createMock(TransferInterface::class); + $this->clientMock = $this->createMock(ClientInterface::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->validatorMock = $this->createMock(ValidatorInterface::class); + + $this->command = new GatewayQueryCommand( + $this->requestBuilderMock, + $this->transferFactoryMock, + $this->clientMock, + $this->loggerMock, + $this->validatorMock + ); + } + + public function testNormalExecution() + { + $buildSubject = [ + 'foo' => '123' + ]; + + $request = [ + 'bar' => '321' + ]; + + $response = [ + 'transaction' => [ + 'transactionType' => 'foo', + 'transactionStatus' => 'bar', + 'responseCode' => 'baz' + ] + ]; + + $validationSubject = $buildSubject; + $validationSubject['response'] = $response; + + $this->requestBuilderMock->method('build') + ->with($buildSubject) + ->willReturn($request); + + $this->transferFactoryMock->method('create') + ->with($request) + ->willReturn($this->transferMock); + + $this->clientMock->method('placeRequest') + ->with($this->transferMock) + ->willReturn($response); + + $this->validatorMock->method('validate') + ->with($validationSubject) + ->willReturn(new Result(true)); + + $result = $this->command->execute($buildSubject); + + $this->assertInstanceOf(ArrayResult::class, $result); + $this->assertEquals($response, $result->get()); + } + + /** + * @expectedExceptionMessage There was an error while trying to process the request. + * @expectedException \Magento\Payment\Gateway\Command\CommandException + */ + public function testExceptionIsThrownAndLoggedWhenRequestFails() + { + $buildSubject = [ + 'foo' => '123' + ]; + + $request = [ + 'bar' => '321' + ]; + + $this->requestBuilderMock->method('build') + ->with($buildSubject) + ->willReturn($request); + + $this->transferFactoryMock->method('create') + ->with($request) + ->willReturn($this->transferMock); + + $e = new \Exception('foobar'); + + $this->clientMock->method('placeRequest') + ->with($this->transferMock) + ->willThrowException($e); + + // assert the exception is logged + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->command->execute($buildSubject); + } + /** + * @expectedExceptionMessage There was an error while trying to process the request. + * @expectedException \Magento\Payment\Gateway\Command\CommandException + */ + public function testExceptionIsThrownWhenResponseIsInvalid() + { + $buildSubject = [ + 'foo' => '123' + ]; + + $request = [ + 'bar' => '321' + ]; + + $response = [ + 'baz' => '456' + ]; + + $validationSubject = $buildSubject; + $validationSubject['response'] = $response; + + $this->requestBuilderMock->method('build') + ->with($buildSubject) + ->willReturn($request); + + $this->transferFactoryMock->method('create') + ->with($request) + ->willReturn($this->transferMock); + + $this->clientMock->method('placeRequest') + ->with($this->transferMock) + ->willReturn($response); + + $this->validatorMock->method('validate') + ->with($validationSubject) + ->willReturn(new Result(false)); + + $this->command->execute($buildSubject); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php new file mode 100644 index 0000000000000..df6d89d7bc585 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\RefundTransactionStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class RefundTransactionStrategyCommandTest extends TestCase +{ + /** + * @var CommandInterface|MockObject + */ + private $commandMock; + + /** + * @var CommandInterface|MockObject + */ + private $transactionDetailsCommandMock; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var RefundTransactionStrategyCommand + */ + private $command; + + /** + * @var ResultInterface|MockObject + */ + private $transactionResultMock; + + protected function setUp() + { + $this->transactionDetailsCommandMock = $this->createMock(CommandInterface::class); + $this->commandMock = $this->createMock(CommandInterface::class); + $this->transactionResultMock = $this->createMock(ResultInterface::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->command = new RefundTransactionStrategyCommand( + $this->commandPoolMock, + new SubjectReader() + ); + } + + public function testCommandWillVoidWhenTransactionIsPendingSettlement() + { + // Assert command is executed + $this->commandMock->expects($this->once()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ['void', $this->commandMock] + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'capturedPendingSettlement' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + public function testCommandWillRefundWhenTransactionIsSettled() + { + // Assert command is executed + $this->commandMock->expects($this->once()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ['refund_settled', $this->commandMock] + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'settledSuccessfully' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + /** + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage This transaction cannot be refunded with its current status. + */ + public function testCommandWillThrowExceptionWhenTransactionIsInInvalidState() + { + // Assert command is never executed + $this->commandMock->expects($this->never()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'somethingIsWrong' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php new file mode 100644 index 0000000000000..da2b953d843b1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ConfigTest extends TestCase +{ + /** + * @var Config + */ + private $model; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + protected function setUp() + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject( + Config::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'methodCode' => Config::METHOD, + ] + ); + } + + /** + * @param $getterName + * @param $configField + * @param $configValue + * @param $expectedValue + * @dataProvider configMapProvider + */ + public function testConfigGetters($getterName, $configField, $configValue, $expectedValue) + { + $this->scopeConfigMock->method('getValue') + ->with($this->getPath($configField), ScopeInterface::SCOPE_STORE, 123) + ->willReturn($configValue); + $this->assertEquals($expectedValue, $this->model->{$getterName}(123)); + } + + /** + * @dataProvider environmentUrlProvider + * @param $environment + * @param $expectedUrl + */ + public function testGetApiUrl($environment, $expectedUrl) + { + $this->scopeConfigMock->method('getValue') + ->with($this->getPath('environment'), ScopeInterface::SCOPE_STORE, 123) + ->willReturn($environment); + $this->assertEquals($expectedUrl, $this->model->getApiUrl(123)); + } + + /** + * @dataProvider environmentSolutionProvider + * @param $environment + * @param $expectedSolution + */ + public function testGetSolutionIdSandbox($environment, $expectedSolution) + { + $this->scopeConfigMock->method('getValue') + ->with($this->getPath('environment'), ScopeInterface::SCOPE_STORE, 123) + ->willReturn($environment); + $this->assertEquals($expectedSolution, $this->model->getSolutionId(123)); + } + + public function configMapProvider() + { + return [ + ['getLoginId', 'login', 'username', 'username'], + ['getEnvironment', 'environment', 'production', 'production'], + ['getClientKey', 'public_client_key', 'abc', 'abc'], + ['getTransactionKey', 'trans_key', 'password', 'password'], + ['getLegacyTransactionHash', 'trans_md5', 'abc123', 'abc123'], + ['getTransactionSignatureKey', 'trans_signature_key', 'abc123', 'abc123'], + ['getPaymentAction', 'payment_action', 'authorize', 'authorize'], + ['shouldEmailCustomer', 'email_customer', true, true], + ['isCvvEnabled', 'cvv_enabled', true, true], + ['getAdditionalInfoKeys', 'paymentInfoKeys', 'a,b,c', ['a', 'b', 'c']], + ['getTransactionInfoSyncKeys', 'transactionSyncKeys', 'a,b,c', ['a', 'b', 'c']], + ]; + } + public function environmentUrlProvider() + { + return [ + ['sandbox', 'https://apitest.authorize.net/xml/v1/request.api'], + ['production', 'https://api.authorize.net/xml/v1/request.api'], + ]; + } + + public function environmentSolutionProvider() + { + return [ + ['sandbox', 'AAA102993'], + ['production', 'AAA175350'], + ]; + } + + /** + * Return config path + * + * @param string $field + * @return string + */ + private function getPath($field) + { + return sprintf(Config::DEFAULT_PATH_PATTERN, Config::METHOD, $field); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/ClientTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/ClientTest.php new file mode 100644 index 0000000000000..4086195ff4c95 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/ClientTest.php @@ -0,0 +1,218 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Http; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Client; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Model\Method\Logger; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Zend_Http_Client; +use Zend_Http_Response; + +class ClientTest extends TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Logger + */ + private $paymentLogger; + + /** + * @var ZendClientFactory + */ + private $httpClientFactory; + + /** + * @var Zend_Http_Client + */ + private $httpClient; + + /** + * @var Zend_Http_Response + */ + private $httpResponse; + + /** + * @var LoggerInterface + */ + private $logger; + + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->paymentLogger = $this->createMock(Logger::class); + $this->httpClientFactory = $this->createMock(ZendClientFactory::class); + $this->httpClient = $this->createMock(Zend_Http_Client::class); + $this->httpResponse = $this->createMock(Zend_Http_Response::class); + $this->httpClientFactory->method('create')->will($this->returnValue($this->httpClient)); + $this->httpClient->method('request') + ->willReturn($this->httpResponse); + /** @var MockObject $logger */ + $this->logger = $this->createMock(LoggerInterface::class); + } + + public function testCanSendRequest() + { + // Assert the raw data was set on the client + $this->httpClient->expects($this->once()) + ->method('setRawData') + ->with( + '{"doSomeThing":{"foobar":"baz"}}', + 'application/json' + ); + + $request = [ + 'payload_type' => 'doSomeThing', + 'foobar' => 'baz' + ]; + // Authorize.net returns a BOM and refuses to fix it + $response = pack('CCC', 0xef, 0xbb, 0xbf) . '{"foo":{"bar":"baz"}}'; + + $this->httpResponse->method('getBody') + ->willReturn($response); + + // Assert the logger was given the data + $this->paymentLogger->expects($this->once()) + ->method('debug') + ->with(['request' => $request, 'response' => '{"foo":{"bar":"baz"}}']); + + /** + * @var $apiClient Client + */ + $apiClient = $this->objectManager->getObject(Client::class, [ + 'httpClientFactory' => $this->httpClientFactory, + 'paymentLogger' => $this->paymentLogger, + 'json' => new Json() + ]); + + $result = $apiClient->placeRequest($this->getTransferObjectMock($request)); + + $this->assertSame('baz', $result['foo']['bar']); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Something went wrong in the payment gateway. + */ + public function testExceptionIsThrownWhenEmptyResponseIsReceived() + { + // Assert the client has the raw data set + $this->httpClient->expects($this->once()) + ->method('setRawData') + ->with( + '{"doSomeThing":{"foobar":"baz"}}', + 'application/json' + ); + + $this->httpResponse->method('getBody') + ->willReturn(''); + + // Assert the exception is given to the logger + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->callback(function ($e) { + return $e instanceof \Exception + && $e->getMessage() === 'Invalid JSON was returned by the gateway'; + })); + + $request = [ + 'payload_type' => 'doSomeThing', + 'foobar' => 'baz' + ]; + + // Assert the logger was given the data + $this->paymentLogger->expects($this->once()) + ->method('debug') + ->with(['request' => $request, 'response' => '']); + + /** + * @var $apiClient Client + */ + $apiClient = $this->objectManager->getObject(Client::class, [ + 'httpClientFactory' => $this->httpClientFactory, + 'paymentLogger' => $this->paymentLogger, + 'logger' => $this->logger, + 'json' => new Json() + ]); + + $apiClient->placeRequest($this->getTransferObjectMock($request)); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Something went wrong in the payment gateway. + */ + public function testExceptionIsThrownWhenInvalidResponseIsReceived() + { + // Assert the client was given the raw data + $this->httpClient->expects($this->once()) + ->method('setRawData') + ->with( + '{"doSomeThing":{"foobar":"baz"}}', + 'application/json' + ); + + $this->httpResponse->method('getBody') + ->willReturn('bad'); + + $request = [ + 'payload_type' => 'doSomeThing', + 'foobar' => 'baz' + ]; + + // Assert the logger was given the data + $this->paymentLogger->expects($this->once()) + ->method('debug') + ->with(['request' => $request, 'response' => 'bad']); + + // Assert the exception was given to the logger + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->callback(function ($e) { + return $e instanceof \Exception + && $e->getMessage() === 'Invalid JSON was returned by the gateway'; + })); + + /** + * @var $apiClient Client + */ + $apiClient = $this->objectManager->getObject(Client::class, [ + 'httpClientFactory' => $this->httpClientFactory, + 'paymentLogger' => $this->paymentLogger, + 'logger' => $this->logger, + 'json' => new Json() + ]); + + $apiClient->placeRequest($this->getTransferObjectMock($request)); + } + + /** + * Creates mock object for TransferInterface. + * + * @return TransferInterface|MockObject + */ + private function getTransferObjectMock(array $data) + { + $transferObjectMock = $this->createMock(TransferInterface::class); + $transferObjectMock->method('getBody') + ->willReturn($data); + + return $transferObjectMock; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/Payload/Filter/RemoveFieldsFilterTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/Payload/Filter/RemoveFieldsFilterTest.php new file mode 100644 index 0000000000000..bcc6279f5b1fe --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/Payload/Filter/RemoveFieldsFilterTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Http\Payload\Filter; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\Filter\RemoveFieldsFilter; +use PHPUnit\Framework\TestCase; + +class RemoveFieldsFilterTest extends TestCase +{ + public function testFilterRemovesFields() + { + $filter = new RemoveFieldsFilter(['foo', 'bar']); + + $actual = $filter->filter([ + 'some' => 123, + 'data' => 321, + 'foo' => 'to', + 'filter' => ['blah'], + 'bar' => 'fields from' + ]); + + $expected = [ + 'some' => 123, + 'data' => 321, + 'filter' => ['blah'], + ]; + + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/TransferFactoryTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/TransferFactoryTest.php new file mode 100644 index 0000000000000..954fd9782bd3f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/TransferFactoryTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Http; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\Filter\RemoveFieldsFilter; +use Magento\AuthorizenetAcceptjs\Gateway\Http\TransferFactory; +use Magento\Payment\Gateway\Http\TransferBuilder; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\FilterInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransferFactoryTest extends TestCase +{ + /** + * @var TransferFactory + */ + private $transferFactory; + + /** + * @var TransferFactory + */ + private $transferMock; + + /** + * @var TransferBuilder|MockObject + */ + private $transferBuilder; + + /** + * @var FilterInterface|MockObject + */ + private $filterMock; + + protected function setUp() + { + $this->transferBuilder = $this->createMock(TransferBuilder::class); + $this->transferMock = $this->createMock(TransferInterface::class); + $this->filterMock = $this->createMock(RemoveFieldsFilter::class); + + $this->transferFactory = new TransferFactory( + $this->transferBuilder, + [$this->filterMock] + ); + } + + public function testCreate() + { + $request = ['data1', 'data2']; + + // Assert the filter was created + $this->filterMock->expects($this->once()) + ->method('filter') + ->with($request) + ->willReturn($request); + + // Assert the body of the transfer was set + $this->transferBuilder->expects($this->once()) + ->method('setBody') + ->with($request) + ->willReturnSelf(); + + $this->transferBuilder->method('build') + ->willReturn($this->transferMock); + + $this->assertEquals($this->transferMock, $this->transferFactory->create($request)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AcceptFdsDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AcceptFdsDataBuilderTest.php new file mode 100644 index 0000000000000..00bb7ee84f98b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AcceptFdsDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AcceptFdsDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\TestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; + +class AcceptFdsDataBuilderTest extends TestCase +{ + /** + * @var AcceptFdsDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new AcceptFdsDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $transactionMock->method('getTxnId') + ->willReturn('foo'); + + $expected = [ + 'heldTransactionRequest' => [ + 'action' => 'approve', + 'refTransId' => 'foo' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php new file mode 100644 index 0000000000000..6ddb30a64af96 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\AddressAdapterInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AddressDataBuilderTest extends TestCase +{ + /** + * @var AddressDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + private $mockAddressData = [ + 'firstName' => [ + 'method' => 'getFirstname', + 'sampleData' => 'John' + ], + 'lastName' => [ + 'method' => 'getLastname', + 'sampleData' => 'Doe' + ], + 'company' => [ + 'method' => 'getCompany', + 'sampleData' => 'Magento' + ], + 'address' => [ + 'method' => 'getStreetLine1', + 'sampleData' => '11501 Domain Dr' + ], + 'city' => [ + 'method' => 'getCity', + 'sampleData' => 'Austin' + ], + 'state' => [ + 'method' => 'getRegionCode', + 'sampleData' => 'TX' + ], + 'zip' => [ + 'method' => 'getPostcode', + 'sampleData' => '78758' + ], + 'country' => [ + 'method' => 'getCountryId', + 'sampleData' => 'US' + ], + ]; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new AddressDataBuilder(new SubjectReader()); + } + + public function testBuildWithBothAddresses() + { + $billingAddress = $this->createAddressMock('billing'); + $shippingAddress = $this->createAddressMock('shipping'); + $this->orderMock->method('getBillingAddress') + ->willReturn($billingAddress); + $this->orderMock->method('getShippingAddress') + ->willReturn($shippingAddress); + $this->orderMock->method('getRemoteIp') + ->willReturn('abc'); + + $buildSubject = [ + 'payment' => $this->paymentDOMock + ]; + + $result = $this->builder->build($buildSubject); + + $this->validateAddressData($result['transactionRequest']['billTo'], 'billing'); + $this->validateAddressData($result['transactionRequest']['shipTo'], 'shipping'); + $this->assertEquals('abc', $result['transactionRequest']['customerIP']); + } + + private function validateAddressData($responseData, $addressPrefix) + { + foreach ($this->mockAddressData as $fieldValue => $field) { + $this->assertEquals($addressPrefix . $field['sampleData'], $responseData[$fieldValue]); + } + } + + private function createAddressMock($prefix) + { + $addressAdapterMock = $this->createMock(AddressAdapterInterface::class); + + foreach ($this->mockAddressData as $field) { + $addressAdapterMock->method($field['method']) + ->willReturn($prefix . $field['sampleData']); + } + + return $addressAdapterMock; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AmountDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AmountDataBuilderTest.php new file mode 100644 index 0000000000000..9da0139302a30 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AmountDataBuilderTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use PHPUnit\Framework\TestCase; + +class AmountDataBuilderTest extends TestCase +{ + /** + * @var AmountDataBuilder + */ + private $builder; + + protected function setUp() + { + $this->builder = new AmountDataBuilder( + new SubjectReader() + ); + } + + public function testBuild() + { + $expected = [ + 'transactionRequest' => [ + 'amount' => '123.45', + ] + ]; + + $buildSubject = [ + 'amount' => 123.45 + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthenticationDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthenticationDataBuilderTest.php new file mode 100644 index 0000000000000..e9588e51b0fc8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthenticationDataBuilderTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AuthenticationDataBuilderTest extends TestCase +{ + /** + * @var AuthenticationDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + /** @var MockObject|SubjectReader subjectReaderMock */ + $this->subjectReaderMock = $this->createMock(SubjectReader::class); + + $this->builder = new AuthenticationDataBuilder($this->subjectReaderMock, $this->configMock); + } + + public function testBuild() + { + $this->configMock->method('getLoginId') + ->willReturn('myloginid'); + $this->configMock->method('getTransactionKey') + ->willReturn('mytransactionkey'); + + $expected = [ + 'merchantAuthentication' => [ + 'name' => 'myloginid', + 'transactionKey' => 'mytransactionkey' + ] + ]; + + $buildSubject = []; + + $this->subjectReaderMock->method('readStoreId') + ->with($buildSubject) + ->willReturn(123); + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthorizationDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthorizationDataBuilderTest.php new file mode 100644 index 0000000000000..438d681a2b5b2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthorizationDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AuthorizeDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AuthorizationDataBuilderTest extends TestCase +{ + /** + * @var AuthorizeDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->passthroughData = new PassthroughDataObject(); + + $this->builder = new AuthorizeDataBuilder( + new SubjectReader(), + $this->passthroughData + ); + } + + public function testBuildWillAddTransactionType() + { + $expected = [ + 'transactionRequest' => [ + 'transactionType' => 'authOnlyTransaction' + ] + ]; + + $buildSubject = [ + 'store_id' => 123, + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + $this->assertEquals('authOnlyTransaction', $this->passthroughData->getData('transactionType')); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php new file mode 100644 index 0000000000000..537a685f1ff7f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\CaptureDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CaptureDataBuilderTest extends TestCase +{ + /** + * @var CaptureDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->passthroughData = new PassthroughDataObject(); + + $this->builder = new CaptureDataBuilder( + new SubjectReader(), + $this->passthroughData + ); + } + + public function testBuildWillCaptureWhenAuthorizeTransactionExists() + { + $transactionMock = $this->createMock(Payment\Transaction::class); + $transactionMock->method('getAdditionalInformation') + ->with('real_transaction_id') + ->willReturn('prevtrans'); + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $expected = [ + 'transactionRequest' => [ + 'transactionType' => 'priorAuthCaptureTransaction', + 'refTransId' => 'prevtrans' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + $this->assertEquals('priorAuthCaptureTransaction', $this->passthroughData->getData('transactionType')); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomSettingsBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomSettingsBuilderTest.php new file mode 100644 index 0000000000000..be7dd7eca1761 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomSettingsBuilderTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CustomSettingsBuilderTest extends TestCase +{ + /** + * @var CustomSettingsBuilder + */ + private $builder; + + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + /** @var MockObject|SubjectReader subjectReaderMock */ + $this->subjectReaderMock = $this->createMock(SubjectReader::class); + $this->subjectReaderMock->method('readStoreId') + ->willReturn('123'); + + $this->builder = new CustomSettingsBuilder($this->subjectReaderMock, $this->configMock); + } + + public function testBuildWithEmailCustomerDisabled() + { + $this->configMock->method('shouldEmailCustomer') + ->with('123') + ->willReturn(false); + + $this->assertEquals([], $this->builder->build([])); + } + + public function testBuildWithEmailCustomerEnabled() + { + $this->configMock->method('shouldEmailCustomer') + ->with('123') + ->willReturn(true); + + $expected = [ + 'transactionRequest' => [ + 'transactionSettings' => [ + 'setting' => [ + [ + 'settingName' => 'emailCustomer', + 'settingValue' => 'true' + ] + ] + ] + ] + ]; + + $this->assertEquals($expected, $this->builder->build([])); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php new file mode 100644 index 0000000000000..7c9116cad54b1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\AddressAdapterInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CustomerDataBuilderTest extends TestCase +{ + /** + * @var CustomerDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new CustomerDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $addressAdapterMock = $this->createMock(AddressAdapterInterface::class); + $addressAdapterMock->method('getEmail') + ->willReturn('foo@bar.com'); + $this->orderMock->method('getBillingAddress') + ->willReturn($addressAdapterMock); + $this->orderMock->method('getCustomerId') + ->willReturn('123'); + + $expected = [ + 'transactionRequest' => [ + 'customer' => [ + 'id' => '123', + 'email' => 'foo@bar.com' + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/OrderDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/OrderDataBuilderTest.php new file mode 100644 index 0000000000000..d66421d48ca8b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/OrderDataBuilderTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class OrderDataBuilderTest extends TestCase +{ + /** + * @var OrderDataBuilder + */ + private $builder; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new OrderDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->orderMock->method('getOrderIncrementId') + ->willReturn('10000015'); + + $expected = [ + 'transactionRequest' => [ + 'order' => [ + 'invoiceNumber' => '10000015' + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'order' => $this->orderMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PassthroughDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PassthroughDataBuilderTest.php new file mode 100644 index 0000000000000..f4c5f56efe890 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PassthroughDataBuilderTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use PHPUnit\Framework\TestCase; + +class PassthroughDataBuilderTest extends TestCase +{ + public function testBuild() + { + $passthroughData = new PassthroughDataObject([ + 'foo' => 'bar', + 'baz' => 'bash' + ]); + $builder = new PassthroughDataBuilder($passthroughData); + + $expected = [ + 'transactionRequest' => [ + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'foo', + 'value' => 'bar' + ], + [ + 'name' => 'baz', + 'value' => 'bash' + ], + ] + ] + ] + ]; + + $this->assertEquals($expected, $builder->build([])); + } + + public function testBuildWithNoData() + { + $passthroughData = new PassthroughDataObject(); + $builder = new PassthroughDataBuilder($passthroughData); + $expected = []; + + $this->assertEquals($expected, $builder->build([])); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php new file mode 100644 index 0000000000000..cf3842b8947bb --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\PaymentDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentDataBuilderTest extends TestCase +{ + /** + * @var PaymentDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new PaymentDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->paymentMock->method('getAdditionalInformation') + ->willReturnMap([ + ['opaqueDataDescriptor', 'foo'], + ['opaqueDataValue', 'bar'] + ]); + + $expected = [ + 'transactionRequest' => [ + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'foo', + 'dataValue' => 'bar' + ] + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'amount' => 123.45 + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PoDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PoDataBuilderTest.php new file mode 100644 index 0000000000000..97b51c1e1807c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PoDataBuilderTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PoDataBuilderTest extends TestCase +{ + /** + * @var PoDataBuilder + */ + private $builder; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new PoDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->paymentMock->method('getPoNumber') + ->willReturn('abc'); + + $expected = [ + 'transactionRequest' => [ + 'poNumber' => 'abc' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundPaymentDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundPaymentDataBuilderTest.php new file mode 100644 index 0000000000000..c1879b3df83a3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundPaymentDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\RefundPaymentDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class RefundPaymentDataBuilderTest extends TestCase +{ + /** + * @var RefundPaymentDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new RefundPaymentDataBuilder( + new SubjectReader() + ); + } + + public function testBuild() + { + $this->paymentMock->method('getAdditionalInformation') + ->with('ccLast4') + ->willReturn('1111'); + + $expected = [ + 'transactionRequest' => [ + 'payment' => [ + 'creditCard' => [ + 'cardNumber' => '1111', + 'expirationDate' => 'XXXX' + ] + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'amount' => 123.45 + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundReferenceTransactionDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundReferenceTransactionDataBuilderTest.php new file mode 100644 index 0000000000000..cf1803005acee --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundReferenceTransactionDataBuilderTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\RefundReferenceTransactionDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\TestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; + +class RefundReferenceTransactionDataBuilderTest extends TestCase +{ + /** + * @var RefundReferenceTransactionDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new RefundReferenceTransactionDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $transactionMock->method('getParentTxnId') + ->willReturn('foo'); + + $expected = [ + 'transactionRequest' => [ + 'refTransId' => 'foo' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundTransactionTypeDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundTransactionTypeDataBuilderTest.php new file mode 100644 index 0000000000000..4e0f5f75fb944 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundTransactionTypeDataBuilderTest.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\RefundTransactionTypeDataBuilder; +use PHPUnit\Framework\TestCase; + +class RefundTransactionTypeDataBuilderTest extends TestCase +{ + private const REQUEST_TYPE_REFUND = 'refundTransaction'; + + public function testBuild() + { + $builder = new RefundTransactionTypeDataBuilder(); + + $expected = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_TYPE_REFUND + ] + ]; + + $this->assertEquals($expected, $builder->build([])); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RequestTypeBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RequestTypeBuilderTest.php new file mode 100644 index 0000000000000..cb03dfc3dac5e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RequestTypeBuilderTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\Request\RequestTypeBuilder; +use PHPUnit\Framework\TestCase; + +class RequestTypeBuilderTest extends TestCase +{ + /** + * @var AuthenticationDataBuilder + */ + private $builder; + + protected function setUp() + { + $this->builder = new RequestTypeBuilder('foo'); + } + + public function testBuild() + { + $expected = [ + 'payload_type' => 'foo' + ]; + + $buildSubject = []; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SaleDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SaleDataBuilderTest.php new file mode 100644 index 0000000000000..407b9bc85a2c5 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SaleDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\SaleDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SaleDataBuilderTest extends TestCase +{ + /** + * @var SaleDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->passthroughData = new PassthroughDataObject(); + + $this->builder = new SaleDataBuilder( + new SubjectReader(), + $this->passthroughData + ); + } + + public function testBuildWillAddTransactionType() + { + $expected = [ + 'transactionRequest' => [ + 'transactionType' => 'authCaptureTransaction' + ] + ]; + + $buildSubject = [ + 'store_id' => 123, + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + $this->assertEquals('authCaptureTransaction', $this->passthroughData->getData('transactionType')); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/ShippingDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/ShippingDataBuilderTest.php new file mode 100644 index 0000000000000..d6525e610a285 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/ShippingDataBuilderTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ShippingDataBuilderTest extends TestCase +{ + /** + * @var v + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var Order + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new ShippingDataBuilder( + new SubjectReader() + ); + } + + public function testBuild() + { + $this->orderMock->method('getBaseShippingAmount') + ->willReturn('43.12'); + + $expected = [ + 'transactionRequest' => [ + 'shipping' => [ + 'amount' => '43.12' + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'order' => $this->orderMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SolutionDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SolutionDataBuilderTest.php new file mode 100644 index 0000000000000..1b06546c2ea8f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SolutionDataBuilderTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Request\SolutionDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SolutionDataBuilderTest extends TestCase +{ + /** + * @var SolutionDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + /** @var MockObject|SubjectReader subjectReaderMock */ + $this->subjectReaderMock = $this->createMock(SubjectReader::class); + + $this->builder = new SolutionDataBuilder($this->subjectReaderMock, $this->configMock); + } + + public function testBuild() + { + $this->subjectReaderMock->method('readStoreId') + ->willReturn('123'); + $this->configMock->method('getSolutionId') + ->with('123') + ->willReturn('solutionid'); + + $expected = [ + 'transactionRequest' => [ + 'solution' => [ + 'id' => 'solutionid', + ] + ] + ]; + + $buildSubject = []; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/StoreConfigBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/StoreConfigBuilderTest.php new file mode 100644 index 0000000000000..2ed0cb13ed624 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/StoreConfigBuilderTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class StoreConfigBuilderTest extends TestCase +{ + /** + * @var StoreConfigBuilder + */ + private $builder; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(InfoInterface::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new StoreConfigBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->orderMock->method('getStoreID') + ->willReturn(123); + + $expected = [ + 'store_id' => 123 + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/TransactionDetailsDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/TransactionDetailsDataBuilderTest.php new file mode 100644 index 0000000000000..03c036c027147 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/TransactionDetailsDataBuilderTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\TransactionDetailsDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\TestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; + +class TransactionDetailsDataBuilderTest extends TestCase +{ + /** + * @var TransactionDetailsDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new TransactionDetailsDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $transactionMock->method('getParentTxnId') + ->willReturn('foo'); + + $expected = [ + 'transId' => 'foo' + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } + + public function testBuildWithIncludedTransactionId() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->expects($this->never()) + ->method('getAuthorizationTransaction'); + + $transactionMock->expects($this->never()) + ->method('getParentTxnId'); + + $expected = [ + 'transId' => 'foo' + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'transactionId' => 'foo' + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/VoidDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/VoidDataBuilderTest.php new file mode 100644 index 0000000000000..84460a1c744b9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/VoidDataBuilderTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\VoidDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class VoidDataBuilderTest extends TestCase +{ + private const REQUEST_TYPE_VOID = 'voidTransaction'; + + /** + * @var VoidDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new VoidDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + $transactionMock->method('getParentTxnId') + ->willReturn('myref'); + + $buildSubject = [ + 'payment' => $this->paymentDOMock + ]; + + $expected = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_TYPE_VOID, + 'refTransId' => 'myref', + ] + ]; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseParentTransactionHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseParentTransactionHandlerTest.php new file mode 100644 index 0000000000000..e9929c631eb15 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseParentTransactionHandlerTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CloseParentTransactionHandlerTest extends TestCase +{ + /** + * @var CloseParentTransactionHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new CloseParentTransactionHandler(new SubjectReader()); + } + + public function testHandleClosesTransactionByDefault() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transactionResponse' => [] + ]; + + // Assert the parent transaction i closed + $this->paymentMock->expects($this->once()) + ->method('setShouldCloseParentTransaction') + ->with(true); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseTransactionHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseTransactionHandlerTest.php new file mode 100644 index 0000000000000..a7093f0dac889 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseTransactionHandlerTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CloseTransactionHandlerTest extends TestCase +{ + /** + * @var CloseTransactionHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new CloseTransactionHandler(new SubjectReader()); + } + + public function testHandleClosesTransactionByDefault() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transactionResponse' => [] + ]; + + // Assert the transaction is closed + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(true); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentResponseHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentResponseHandlerTest.php new file mode 100644 index 0000000000000..d051c7d2910a5 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentResponseHandlerTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentResponseHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentResponseHandlerTest extends TestCase +{ + private const RESPONSE_CODE_APPROVED = 1; + private const RESPONSE_CODE_HELD = 4; + + /** + * @var PaymentResponseHandler + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new PaymentResponseHandler(new SubjectReader()); + } + + public function testHandleDefaultResponse() + { + $this->paymentMock->method('getAdditionalInformation') + ->with('ccLast4') + ->willReturn('1234'); + // Assert the avs code is saved + $this->paymentMock->expects($this->once()) + ->method('setCcAvsStatus') + ->with('avshurray'); + $this->paymentMock->expects($this->once()) + ->method('setCcLast4') + ->with('1234'); + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(false); + + $response = [ + 'transactionResponse' => [ + 'avsResultCode' => 'avshurray', + 'responseCode' => self::RESPONSE_CODE_APPROVED, + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } + + public function testHandleHeldResponse() + { + // Assert the avs code is saved + $this->paymentMock->expects($this->once()) + ->method('setCcAvsStatus') + ->with('avshurray'); + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(false); + // opaque data wasn't provided + $this->paymentMock->expects($this->never()) + ->method('setAdditionalInformation'); + // Assert the payment is flagged for review + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionPending') + ->with(true) + ->willReturnSelf(); + $this->paymentMock->expects($this->once()) + ->method('setIsFraudDetected') + ->with(true); + + $response = [ + 'transactionResponse' => [ + 'avsResultCode' => 'avshurray', + 'responseCode' => self::RESPONSE_CODE_HELD, + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php new file mode 100644 index 0000000000000..a52a1b317fbb7 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentReviewStatusHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentReviewStatusHandlerTest extends TestCase +{ + /** + * @var PaymentReviewStatusHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new PaymentReviewStatusHandler(new SubjectReader()); + } + + public function testApprovesPayment() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transaction' => [ + 'transactionStatus' => 'approvedOrSomething', + ] + ]; + + // Assert payment is handled correctly + $this->paymentMock->expects($this->exactly(2)) + ->method('setData') + ->withConsecutive( + ['is_transaction_denied', false], + ['is_transaction_approved', true] + ); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } + + /** + * @param string $status + * @dataProvider declinedTransactionStatusesProvider + */ + public function testDeniesPayment(string $status) + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transaction' => [ + 'transactionStatus' => $status, + ] + ]; + + // Assert payment is handled correctly + $this->paymentMock->expects($this->exactly(2)) + ->method('setData') + ->withConsecutive( + ['is_transaction_denied', true], + ['is_transaction_approved', false] + ); + $this->handler->handle($subject, $response); + } + + /** + * @param string $status + * @dataProvider pendingTransactionStatusesProvider + */ + public function testDoesNothingWhenPending(string $status) + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transaction' => [ + 'transactionStatus' => $status, + ] + ]; + + // Assert payment is handled correctly + $this->paymentMock->expects($this->never()) + ->method('setData'); + + $this->handler->handle($subject, $response); + } + + public function pendingTransactionStatusesProvider() + { + return [ + ['FDSPendingReview'], + ['FDSAuthorizedPendingReview'] + ]; + } + + public function declinedTransactionStatusesProvider() + { + return [ + ['void'], + ['declined'] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionDetailsResponseHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionDetailsResponseHandlerTest.php new file mode 100644 index 0000000000000..016e3a1e95383 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionDetailsResponseHandlerTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionDetailsResponseHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransactionDetailsResponseHandlerTest extends TestCase +{ + /** + * @var TransactionDetailsResponseHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->configMock = $this->createMock(Config::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new TransactionDetailsResponseHandler(new SubjectReader(), $this->configMock); + } + + public function testHandle() + { + $subject = [ + 'payment' => $this->paymentDOMock, + 'store_id' => 123, + ]; + $response = [ + 'transactionResponse' => [ + 'dontsaveme' => 'dontdoti', + 'abc' => 'foobar', + ] + ]; + + // Assert the information comes from the right store config + $this->configMock->method('getAdditionalInfoKeys') + ->with(123) + ->willReturn(['abc']); + + // Assert the payment has the most recent information always set on it + $this->paymentMock->expects($this->once()) + ->method('setAdditionalInformation') + ->with('abc', 'foobar'); + // Assert the transaction has the raw details from the transaction + $this->paymentMock->expects($this->once()) + ->method('setTransactionAdditionalInfo') + ->with('raw_details_info', ['abc' => 'foobar']); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php new file mode 100644 index 0000000000000..710f995918495 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransactionIdHandlerTest extends TestCase +{ + /** + * @var TransactionIdHandler + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new TransactionIdHandler(new SubjectReader()); + } + + public function testHandleDefaultResponse() + { + $this->paymentMock->method('getParentTransactionId') + ->willReturn(null); + // Assert the id is set + $this->paymentMock->expects($this->once()) + ->method('setTransactionId') + ->with('thetransid'); + // Assert the id is set in the additional info for later + $this->paymentMock->expects($this->once()) + ->method('setTransactionAdditionalInfo') + ->with('real_transaction_id', 'thetransid'); + + $response = [ + 'transactionResponse' => [ + 'transId' => 'thetransid', + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } + + public function testHandleDifferenceInTransactionId() + { + $this->paymentMock->method('getParentTransactionId') + ->willReturn('somethingElse'); + // Assert the id is set + $this->paymentMock->expects($this->once()) + ->method('setTransactionId') + ->with('thetransid'); + + $response = [ + 'transactionResponse' => [ + 'transId' => 'thetransid', + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/VoidResponseHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/VoidResponseHandlerTest.php new file mode 100644 index 0000000000000..f99da2b2ec90b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/VoidResponseHandlerTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\VoidResponseHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class VoidResponseHandlerTest extends TestCase +{ + /** + * @var VoidResponseHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new VoidResponseHandler(new SubjectReader()); + } + + public function testHandle() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transactionResponse' => [ + 'transId' => 'abc123', + ] + ]; + + // Assert the transaction is closed + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(true); + // Assert the parent transaction is closed + $this->paymentMock->expects($this->once()) + ->method('setShouldCloseParentTransaction') + ->with(true); + // Assert the authorize.net transaction id is saved + $this->paymentMock->expects($this->once()) + ->method('setTransactionAdditionalInfo') + ->with('real_transaction_id', 'abc123'); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/SubjectReaderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/SubjectReaderTest.php new file mode 100644 index 0000000000000..42219024badbf --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/SubjectReaderTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use PHPUnit\Framework\TestCase; + +class SubjectReaderTest extends TestCase +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->subjectReader = new SubjectReader(); + } + + public function testReadPayment(): void + { + $paymentDO = $this->createMock(PaymentDataObjectInterface::class); + + $this->assertSame($paymentDO, $this->subjectReader->readPayment(['payment' => $paymentDO])); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Payment data object should be provided + */ + public function testReadPaymentThrowsExceptionWhenNotAPaymentObject(): void + { + $this->subjectReader->readPayment(['payment' => 'nope']); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Payment data object should be provided + */ + public function testReadPaymentThrowsExceptionWhenNotSet(): void + { + $this->subjectReader->readPayment([]); + } + + public function testReadResponse(): void + { + $expected = ['foo' => 'bar']; + + $this->assertSame($expected, $this->subjectReader->readResponse(['response' => $expected])); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Response does not exist + */ + public function testReadResponseThrowsExceptionWhenNotAvailable(): void + { + $this->subjectReader->readResponse([]); + } + + public function testReadStoreId(): void + { + $this->assertEquals(123, $this->subjectReader->readStoreId(['store_id' => '123'])); + } + + public function testReadStoreIdFromOrder(): void + { + $paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $orderMock = $this->createMock(OrderAdapterInterface::class); + $paymentDOMock->method('getOrder') + ->willReturn($orderMock); + $orderMock->method('getStoreID') + ->willReturn('123'); + + $result = $this->subjectReader->readStoreId([ + 'payment' => $paymentDOMock + ]); + + $this->assertEquals(123, $result); + } + + public function testReadLoginId(): void + { + $this->assertEquals('abc', $this->subjectReader->readLoginId([ + 'merchantAuthentication' => ['name' => 'abc'] + ])); + } + + public function testReadTransactionKey(): void + { + $this->assertEquals('abc', $this->subjectReader->readTransactionKey([ + 'merchantAuthentication' => ['transactionKey' => 'abc'] + ])); + } + + public function testReadAmount(): void + { + $this->assertSame('123.12', $this->subjectReader->readAmount(['amount' => 123.12])); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Amount should be provided + */ + public function testReadAmountThrowsExceptionWhenNotAvailable(): void + { + $this->subjectReader->readAmount([]); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php new file mode 100644 index 0000000000000..347cd071acc3a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class GeneralResponseValidatorTest extends TestCase +{ + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactoryMock; + + /** + * @var GeneralResponseValidator + */ + private $validator; + + protected function setUp() + { + $this->resultFactoryMock = $this->createMock(ResultInterfaceFactory::class); + $this->validator = new GeneralResponseValidator($this->resultFactoryMock, new SubjectReader()); + } + + public function testValidateParsesSuccess() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'foo', + 'text' => 'bar' + ] + ] + ] + ] + ]); + + $this->assertTrue($args['isValid']); + $this->assertEmpty($args['errorCodes']); + $this->assertEmpty($args['failsDescription']); + } + + public function testValidateParsesErrors() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'errors' => [ + 'resultCode' => 'Error', + 'error' => [ + [ + 'errorCode' => 'foo', + 'errorText' => 'bar' + ] + ] + ] + ] + ]); + + $this->assertFalse($args['isValid']); + $this->assertSame(['foo'], $args['errorCodes']); + $this->assertSame(['bar'], $args['failsDescription']); + } + + public function testValidateParsesMessages() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'messages' => [ + 'resultCode' => 'Error', + 'message' => [ + [ + 'code' => 'foo', + 'text' => 'bar' + ] + ] + ] + ] + ]); + + $this->assertFalse($args['isValid']); + $this->assertSame(['foo'], $args['errorCodes']); + $this->assertSame(['bar'], $args['failsDescription']); + } + + public function testValidateParsesErrorsWhenOnlyOneIsReturned() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'messages' => [ + 'resultCode' => 'Error', + 'message' => [ + 'code' => 'foo', + 'text' => 'bar' + ] + ] + ] + ]); + + $this->assertFalse($args['isValid']); + $this->assertSame(['foo'], $args['errorCodes']); + $this->assertSame(['bar'], $args['failsDescription']); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionHashValidatorTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionHashValidatorTest.php new file mode 100644 index 0000000000000..fb3f9d0520d49 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionHashValidatorTest.php @@ -0,0 +1,280 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionHashValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransactionHashValidatorTest extends TestCase +{ + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactoryMock; + + /** + * @var TransactionHashValidator + */ + private $validator; + + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var ResultInterface + */ + private $resultMock; + + protected function setUp() + { + $this->resultFactoryMock = $this->createMock(ResultInterfaceFactory::class); + $this->configMock = $this->createMock(Config::class); + $this->resultMock = $this->createMock(ResultInterface::class); + + $this->validator = new TransactionHashValidator( + $this->resultFactoryMock, + new SubjectReader(), + $this->configMock + ); + } + + /** + * @param $response + * @param $isValid + * @param $errorCodes + * @param $errorDescriptions + * @dataProvider sha512ResponseProvider + */ + public function testValidateSha512HashScenarios( + $response, + $isValid, + $errorCodes, + $errorDescriptions + ) { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->resultMock); + + $this->configMock->method('getTransactionSignatureKey') + ->willReturn('abc'); + $this->configMock->method('getLoginId') + ->willReturn('username'); + + $this->validator->validate($response); + + $this->assertSame($isValid, $args['isValid']); + $this->assertEquals($errorCodes, $args['errorCodes']); + $this->assertEquals($errorDescriptions, $args['failsDescription']); + } + + /** + * @param $response + * @param $isValid + * @param $errorCodes + * @param $errorDescriptions + * @dataProvider md5ResponseProvider + */ + public function testValidateMd5HashScenarios( + $response, + $isValid, + $errorCodes, + $errorDescriptions + ) { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->resultMock); + + $this->configMock->method('getLegacyTransactionHash') + ->willReturn('abc'); + $this->configMock->method('getLoginId') + ->willReturn('username'); + + $this->validator->validate($response); + + $this->assertSame($isValid, $args['isValid']); + $this->assertEquals($errorCodes, $args['errorCodes']); + $this->assertEquals($errorDescriptions, $args['failsDescription']); + } + + public function md5ResponseProvider() + { + return [ + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'transHash' => 'C8675D9F7BE7BE4A04C18EA1B6F7B6FD' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'transHash' => 'C8675D9F7BE7BE4A04C18EA1B6F7B6FD' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transHash' => 'bad' + ] + ] + ], + false, + ['ETHV'], + ['The authenticity of the gateway response could not be verified.'] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'refTransID' => '123', + 'transId' => '123', + 'transHash' => 'C8675D9F7BE7BE4A04C18EA1B6F7B6FD' + ] + ] + ], + true, + [], + [] + ], + ]; + } + + public function sha512ResponseProvider() + { + return [ + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'refTransID' => '123', + 'transHashSha2' => 'CC0FF465A081D98FFC6E502C40B2DCC7655ACF591F859135B6E66558D' + . '41E3A2C654D5A2ACF4749104F3133711175C232C32676F79F70211C2984B21A33D30DEE' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '0', + 'refTransID' => '123', + 'transHashSha2' => '563D42F4A5189F74334088EF6A02E84F320CD8C005FB0DC436EF96084D' + . 'FAC0C76DE081DFC58A3BF825465C63B7F38E4D463025EAC44597A68C024CBBCE7A3159' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transId' => '0', + 'transHashSha2' => 'DEE5309078D9F7A68BA4F706FB3E58618D3991A6A5E4C39DCF9C49E693' + . '673C38BD6BB15C235263C549A6B5F0B6D7019EC729E0C275C9FEA37FB91F8B612D0A5D' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'transHashSha2' => '1DBD16DED0DA02F52A22A9AD71A49F70BD2ECD42437552889912DD5CE' + . 'CBA0E09A5E8E6221DA74D98A46E5F77F7774B6D9C39CADF3E9A33D85870A6958DA7C8B2' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'refTransID' => '0', + 'transHashSha2' => '1DBD16DED0DA02F52A22A9AD71A49F70BD2ECD42437552889912DD5CE' + . 'CBA0E09A5E8E6221DA74D98A46E5F77F7774B6D9C39CADF3E9A33D85870A6958DA7C8B2' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transHashSha2' => 'bad' + ] + ] + ], + false, + ['ETHV'], + ['The authenticity of the gateway response could not be verified.'] + ], + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php new file mode 100644 index 0000000000000..c59cf00899af2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php @@ -0,0 +1,231 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionResponseValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Tests for the transaction response validator + */ +class TransactionResponseValidatorTest extends TestCase +{ + private const RESPONSE_CODE_APPROVED = 1; + private const RESPONSE_CODE_HELD = 4; + private const RESPONSE_CODE_DENIED = 2; + private const RESPONSE_REASON_CODE_APPROVED = 1; + private const RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED = 252; + private const RESPONSE_REASON_CODE_PENDING_REVIEW = 253; + private const ERROR_CODE_AVS_MISMATCH = 27; + + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactoryMock; + + /** + * @var TransactionResponseValidator + */ + private $validator; + + /** + * @var ResultInterface + */ + private $resultMock; + + protected function setUp() + { + $this->resultFactoryMock = $this->createMock(ResultInterfaceFactory::class); + $this->resultMock = $this->createMock(ResultInterface::class); + + $this->validator = new TransactionResponseValidator( + $this->resultFactoryMock, + new SubjectReader() + ); + } + + /** + * @param $transactionResponse + * @param $isValid + * @param $errorCodes + * @param $errorMessages + * @dataProvider scenarioProvider + */ + public function testValidateScenarios($transactionResponse, $isValid, $errorCodes, $errorMessages) + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->resultMock); + + $this->validator->validate([ + 'response' => [ + 'transactionResponse' => $transactionResponse + ] + ]); + + $this->assertEquals($isValid, $args['isValid']); + $this->assertEquals($errorCodes, $args['errorCodes']); + $this->assertEquals($errorMessages, $args['failsDescription']); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function scenarioProvider() + { + return [ + // Test for acceptable reason codes + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_APPROVED, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_HELD, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_APPROVED, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_HELD, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_HELD, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED, + ] + ] + ], + true, + [], + [] + ], + + // Test for reason codes that aren't acceptable + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + [ + 'description' => 'bar', + 'code' => 'foo', + ] + ] + ] + ], + false, + ['foo'], + ['bar'] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + // Alternate, non-array sytax + 'text' => 'bar', + 'code' => 'foo', + ] + ] + ], + false, + ['foo'], + ['bar'] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_DENIED, + 'errors' => [ + [ + 'errorCode' => self::ERROR_CODE_AVS_MISMATCH, + 'errorText' => 'bar' + ] + ] + ], + false, + [self::ERROR_CODE_AVS_MISMATCH], + ['bar'] + ], + // This validator only cares about successful edge cases so test for default behavior + [ + [ + 'responseCode' => 'foo', + ], + false, + [], + [] + ], + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Model/Ui/ConfigProviderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Model/Ui/ConfigProviderTest.php new file mode 100644 index 0000000000000..dea4557fd584c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Model/Ui/ConfigProviderTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Model\Ui; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider; +use Magento\Quote\Api\Data\CartInterface; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ConfigProviderTest extends TestCase +{ + /** + * @var CartInterface|MockObject|InvocationMocker + */ + private $cart; + + /** + * @var Config|MockObject|InvocationMocker + */ + private $config; + + /** + * @var ConfigProvider + */ + private $provider; + + protected function setUp() + { + $this->cart = $this->createMock(CartInterface::class); + $this->config = $this->createMock(Config::class); + $this->provider = new ConfigProvider($this->config, $this->cart); + } + + public function testProviderRetrievesValues() + { + $this->cart->method('getStoreId') + ->willReturn('123'); + + $this->config->method('getClientKey') + ->with('123') + ->willReturn('foo'); + + $this->config->method('getLoginId') + ->with('123') + ->willReturn('bar'); + + $this->config->method('getEnvironment') + ->with('123') + ->willReturn('baz'); + + $this->config->method('isCvvEnabled') + ->with('123') + ->willReturn(false); + + $expected = [ + 'payment' => [ + Config::METHOD => [ + 'clientKey' => 'foo', + 'apiLoginID' => 'bar', + 'environment' => 'baz', + 'useCvv' => false, + ] + ] + ]; + + $this->assertEquals($expected, $this->provider->getConfig()); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php new file mode 100644 index 0000000000000..ebb95263f54d2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Observer; + +use Magento\Framework\DataObject; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\AuthorizenetAcceptjs\Observer\DataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; +use PHPUnit\Framework\TestCase; + +class DataAssignObserverTest extends TestCase +{ + public function testExecuteSetsProperData() + { + $additionalInfo = [ + 'opaqueDataDescriptor' => 'foo', + 'opaqueDataValue' => 'bar', + 'ccLast4' => '1234' + ]; + + $observerContainer = $this->createMock(Observer::class); + $event = $this->createMock(Event::class); + $paymentInfoModel = $this->createMock(InfoInterface::class); + $dataObject = new DataObject([ + PaymentInterface::KEY_ADDITIONAL_DATA => $additionalInfo + ]); + $observerContainer->method('getEvent') + ->willReturn($event); + $event->method('getDataByKey') + ->willReturnMap( + [ + [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], + [AbstractDataAssignObserver::DATA_CODE, $dataObject] + ] + ); + $paymentInfoModel->expects($this->at(0)) + ->method('setAdditionalInformation') + ->with('opaqueDataDescriptor', 'foo'); + $paymentInfoModel->expects($this->at(1)) + ->method('setAdditionalInformation') + ->with('opaqueDataValue', 'bar'); + $paymentInfoModel->expects($this->at(2)) + ->method('setAdditionalInformation') + ->with('ccLast4', '1234'); + + $observer = new DataAssignObserver(); + $observer->execute($observerContainer); + } + + public function testDoestSetDataWhenEmpty() + { + $observerContainer = $this->createMock(Observer::class); + $event = $this->createMock(Event::class); + $paymentInfoModel = $this->createMock(InfoInterface::class); + $observerContainer->method('getEvent') + ->willReturn($event); + $event->method('getDataByKey') + ->willReturnMap( + [ + [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], + [AbstractDataAssignObserver::DATA_CODE, new DataObject()] + ] + ); + $paymentInfoModel->expects($this->never()) + ->method('setAdditionalInformation'); + + $observer = new DataAssignObserver(); + $observer->execute($observerContainer); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Setup/Patch/Data/CopyCurrentConfigTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Setup/Patch/Data/CopyCurrentConfigTest.php new file mode 100644 index 0000000000000..5ac8a6ca9b3f6 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Setup/Patch/Data/CopyCurrentConfigTest.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Setup\Patch\Data; + +use Magento\AuthorizenetAcceptjs\Setup\Patch\Data\CopyCurrentConfig; +use Magento\Config\Model\ResourceModel\Config as ResourceConfig; +use Magento\Framework\App\Config; +use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Setup\Module\DataSetup; +use Magento\Setup\Model\ModuleContext; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; +use PHPUnit\Framework\TestCase; + +class CopyCurrentConfigTest extends TestCase +{ + /** + * @var \Magento\Framework\App\Config + */ + private $scopeConfig; + + /** + * @var \Magento\Config\Model\ResourceModel\Config + */ + private $resourceConfig; + + /** + * @var \Magento\Framework\Encryption\Encryptor + */ + private $encryptor; + + /** + * @var \Magento\Setup\Module\DataSetup + */ + private $setup; + + /** + * @var \Magento\Setup\Model\ModuleContext + */ + private $context; + + /** + * @var \Magento\Store\Model\StoreManager + */ + private $storeManager; + + /** + * @var \Magento\Store\Model\Website + */ + private $website; + + protected function setUp(): void + { + $this->scopeConfig = $this->createMock(Config::class); + $this->resourceConfig = $this->createMock(ResourceConfig::class); + $this->encryptor = $this->createMock(Encryptor::class); + $this->setup = $this->createMock(DataSetup::class); + + $this->setup->expects($this->once()) + ->method('startSetup') + ->willReturn(null); + + $this->setup->expects($this->once()) + ->method('endSetup') + ->willReturn(null); + + $this->context = $this->createMock(ModuleContext::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->website = $this->createMock(Website::class); + } + + public function testMigrateData(): void + { + $this->scopeConfig->expects($this->exactly(26)) + ->method('getValue') + ->willReturn('TestValue'); + + $this->resourceConfig->expects($this->exactly(26)) + ->method('saveConfig') + ->willReturn(null); + + $this->encryptor->expects($this->exactly(6)) + ->method('encrypt') + ->willReturn('TestValue'); + + $this->website->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->storeManager->expects($this->once()) + ->method('getWebsites') + ->willReturn([$this->website]); + + $objectManager = new ObjectManager($this); + + $installer = $objectManager->getObject( + CopyCurrentConfig::class, + [ + 'moduleDataSetup' => $this->setup, + 'scopeConfig' => $this->scopeConfig, + 'resourceConfig' => $this->resourceConfig, + 'encryptor' => $this->encryptor, + 'storeManager' => $this->storeManager + ] + ); + + $installer->apply($this->context); + } + + public function testMigrateDataNullFields(): void + { + $this->scopeConfig->expects($this->exactly(13)) + ->method('getValue') + ->will($this->onConsecutiveCalls(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + + $this->resourceConfig->expects($this->exactly(10)) + ->method('saveConfig') + ->willReturn(null); + + $this->encryptor->expects($this->never()) + ->method('encrypt'); + + $this->storeManager->expects($this->once()) + ->method('getWebsites') + ->willReturn([]); + + $objectManager = new ObjectManager($this); + + $installer = $objectManager->getObject( + CopyCurrentConfig::class, + [ + 'moduleDataSetup' => $this->setup, + 'scopeConfig' => $this->scopeConfig, + 'resourceConfig' => $this->resourceConfig, + 'encryptor' => $this->encryptor, + 'storeManager' => $this->storeManager + ] + ); + + $installer->apply($this->context); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/composer.json b/app/code/Magento/AuthorizenetAcceptjs/composer.json new file mode 100644 index 0000000000000..be2cd6d4e70f8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/composer.json @@ -0,0 +1,31 @@ +{ + "name": "magento/module-authorizenet-acceptjs", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-payment": "*", + "magento/module-sales": "*", + "magento/module-config": "*", + "magento/module-backend": "*", + "magento/module-checkout": "*", + "magento/module-store": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AuthorizenetAcceptjs\\": "" + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..320f8f79ee28a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml @@ -0,0 +1,14 @@ +<?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\AuthorizenetAcceptjs\Block\Payment"> + <arguments> + <argument name="config" xsi:type="object">Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..279a904d916a2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml @@ -0,0 +1,118 @@ +<?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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="payment"> + <group id="authorizenet_acceptjs" translate="label" type="text" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Authorize.Net</label> + <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <requires> + <group id="authorizenet_acceptjs_required"/> + </requires> + </field> + <group id="required" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="5"> + <label>Basic Authorize.Net Settings</label> + <attribute type="expanded">1</attribute> + <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> + <field id="title" translate="label" type="text" sortOrder="10" showInDefault="10" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Title</label> + <config_path>payment/authorizenet_acceptjs/title</config_path> + </field> + <field id="environment" translate="label" type="select" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Environment</label> + <source_model>Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\Environment</source_model> + <config_path>payment/authorizenet_acceptjs/environment</config_path> + </field> + <field id="payment_action" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Payment Action</label> + <source_model>Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\PaymentAction</source_model> + <config_path>payment/authorizenet_acceptjs/payment_action</config_path> + </field> + <field id="login" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>API Login ID</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/login</config_path> + </field> + <field id="trans_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Transaction Key</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/trans_key</config_path> + </field> + <field id="public_client_key" translate="label" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Public Client Key</label> + <config_path>payment/authorizenet_acceptjs/public_client_key</config_path> + </field> + <field id="trans_signature_key" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Signature Key</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/trans_signature_key</config_path> + </field> + <field id="trans_md5" translate="label" type="obscure" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Merchant MD5 (deprecated)</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/trans_md5</config_path> + </field> + </group> + <group id="advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="0" sortOrder="20"> + <label>Advanced Authorize.Net Settings</label> + <attribute type="expanded">0</attribute> + <field id="currency" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Accepted Currency</label> + <source_model>Magento\Config\Model\Config\Source\Locale\Currency</source_model> + <config_path>payment/authorizenet_acceptjs/currency</config_path> + </field> + <field id="debug" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Debug</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>payment/authorizenet_acceptjs/debug</config_path> + </field> + <field id="email_customer" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Email Customer</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>payment/authorizenet_acceptjs/email_customer</config_path> + </field> + <field id="cvv_enabled" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Enable Credit Card Verification Field</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>payment/authorizenet_acceptjs/cvv_enabled</config_path> + </field> + <field id="cctypes" translate="label" type="multiselect" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Credit Card Types</label> + <source_model>Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\Cctype</source_model> + <config_path>payment/authorizenet_acceptjs/cctypes</config_path> + </field> + <field id="allowspecific" translate="label" type="allowspecific" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Payment from Applicable Countries</label> + <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> + <config_path>payment/authorizenet_acceptjs/allowspecific</config_path> + </field> + <field id="specificcountry" translate="label" type="multiselect" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Payment from Specific Countries</label> + <source_model>Magento\Directory\Model\Config\Source\Country</source_model> + <config_path>payment/authorizenet_acceptjs/specificcountry</config_path> + </field> + <field id="min_order_total" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Minimum Order Total</label> + <config_path>payment/authorizenet_acceptjs/min_order_total</config_path> + </field> + <field id="max_order_total" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Maximum Order Total</label> + <config_path>payment/authorizenet_acceptjs/max_order_total</config_path> + </field> + <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Sort Order</label> + <frontend_class>validate-number</frontend_class> + <config_path>payment/authorizenet_acceptjs/sort_order</config_path> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/authorizenet_acceptjs_error_mapping.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/authorizenet_acceptjs_error_mapping.xml new file mode 100644 index 0000000000000..507a9b14f917b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/authorizenet_acceptjs_error_mapping.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/error_mapping.xsd"> + <message_list> + <message code="E00003" translate="true">Invalid request to gateway.</message> + <message code="E00007" translate="true">Invalid gateway credentials.</message> + <message code="E00027" translate="true">Transaction has been declined. Please try again later.</message> + <message code="ETHV" translate="true">The authenticity of the gateway response could not be verified.</message> + </message_list> +</mapping> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml new file mode 100644 index 0000000000000..7324421d3c14b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml @@ -0,0 +1,56 @@ +<?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:module:Magento_Store:etc/config.xsd"> + <default> + <dev> + <js> + <minify_exclude> + <authorizenet_acceptjs>\.authorize\.net/v1/Accept</authorizenet_acceptjs> + </minify_exclude> + </js> + </dev> + <payment> + <authorizenet_acceptjs> + <active>0</active> + <cctypes>AE,VI,MC,DI,JCB,DN</cctypes> + <debug>0</debug> + <can_use_checkout>1</can_use_checkout> + <can_use_internal>1</can_use_internal> + <can_capture_partial>0</can_capture_partial> + <can_authorize>1</can_authorize> + <can_refund>1</can_refund> + <can_capture>1</can_capture> + <can_void>1</can_void> + <can_accept_payment>1</can_accept_payment> + <can_deny_payment>1</can_deny_payment> + <can_cancel>1</can_cancel> + <can_review_payment>1</can_review_payment> + <can_edit>1</can_edit> + <can_fetch_transaction_info>1</can_fetch_transaction_info> + <can_fetch_transaction_information>1</can_fetch_transaction_information> + <model>AuthorizenetAcceptjsFacade</model> + <email_customer>0</email_customer> + <login backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <order_status>processing</order_status> + <payment_action>authorize</payment_action> + <title>Credit Card (Authorize.Net) + 1 + + + + + 0 + USD + production + authCode,avsResultCode,cvvResultCode,cavvResultCode + accountType,ccLast4,authCode,avsResultCode,cvvResultCode,cavvResultCode + transactionStatus,responseCode,responseReasonCode,authCode,AVSResponse,cardCodeResponse,CAVVResponse + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml new file mode 100644 index 0000000000000..cf10557d3869a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml @@ -0,0 +1,428 @@ + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + + + + + authorizenet_acceptjs + Magento\AuthorizenetAcceptjs\Block\Form + AuthorizenetAcceptjsInfoBlock + AuthorizenetAcceptjsValueHandlerPool + AuthorizenetAcceptjsValidatorPool + AuthorizenetAcceptjsCommandPool + + + + + + AuthorizenetAcceptjsAuthorizeCommand + AuthorizenetAcceptjsCaptureCommand + AuthorizenetAcceptjsSaleCommand + AuthorizenetAcceptjsSettleCommand + AuthorizenetAcceptjsVoidCommand + AuthorizenetAcceptjsRefundCommand + AuthorizenetAcceptjsRefundSettledCommand + AuthorizenetAcceptjsCancelCommand + AuthorizenetAcceptjsAcceptPaymentCommand + AuthorizenetAcceptjsAcceptFdsCommand + AuthorizenetAcceptjsCancelCommand + AuthorizenetAcceptjsTransactionDetailsCommand + AuthorizenetAcceptjsFetchTransactionInfoCommand + + + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator + + + + + + + true + + + Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator + Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionResponseValidator + Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionHashValidator + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + + + + AuthorizenetAcceptjsCountryValidator + + + + + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsTransactionDetailsRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsTransactionDetailsValidator + + + + + AuthorizenetAcceptjsAuthorizeRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsAuthorizationHandler + AuthorizenetAcceptjsTransactionValidator + AuthorizenetAcceptjsVirtualErrorMessageMapper + + + + + AuthorizenetAcceptjsAcceptsFdsRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsAcceptsFdsRequestValidator + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsSaleRequest + AuthorizenetAcceptjsSaleHandler + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsRefundRequest + AuthorizenetAcceptjsRefundSettledHandler + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsCaptureRequest + AuthorizenetAcceptjsCaptureTransactionHandler + + + + + AuthorizenetAcceptjsVoidRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsVoidHandler + AuthorizenetAcceptjsTransactionValidator + AuthorizenetAcceptjsVirtualErrorMessageMapper + + + + + AuthorizenetAcceptjsCancelHandler + + + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentReviewStatusHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentResponseHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionDetailsResponseHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\VoidResponseHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + + + + + + + + + + + AuthorizenetAcceptjsTransactionDetailsRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\TransactionDetailsDataBuilder + + + + + + + AuthorizenetAcceptjsAcceptsFdsRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AcceptFdsDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthorizeDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PaymentDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\SolutionDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Request\SaleDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\RefundTransactionTypeDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\RefundPaymentDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\RefundReferenceTransactionDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CaptureDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\VoidDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + + + createTransactionRequest + + + + + getTransactionDetailsRequest + + + + + updateHeldTransactionRequest + + + + + + + + authorizenet_acceptjs_error_mapping.xml + + + + + AuthorizenetAcceptjsErrorMappingConfigReader + authorizenet_acceptjs_error_mapper + + + + + AuthorizenetAcceptjsErrorMappingData + + + + + + + AuthorizenetAcceptjsPaymentReviewStatusHandler + + + + + + AuthorizenetAcceptjsCommandManager + + + + + + + 1 + 1 + 1 + 1 + 1 + 1 + + + 1 + 1 + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + + + + AuthorizenetAcceptjsDefaultValueHandler + + + + + + AuthorizenetAcceptjsCommandPool + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + + + AuthorizenetAcceptjsLogger + + + + + + store_id + + + + + + + AuthorizenetAcceptjsRemoveStoreConfigFilter + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/events.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/events.xml new file mode 100644 index 0000000000000..93dc448d1d895 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/events.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/frontend/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/frontend/di.xml new file mode 100644 index 0000000000000..8b0e570abbd2e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/frontend/di.xml @@ -0,0 +1,30 @@ + + + + + + + 1 + + + + + + + Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/module.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/module.xml new file mode 100644 index 0000000000000..6bc8fe3c4daee --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/module.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml new file mode 100644 index 0000000000000..b9f8d40b03006 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml @@ -0,0 +1,15 @@ + + + + + + 0 + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv new file mode 100644 index 0000000000000..da518301652f4 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv @@ -0,0 +1,21 @@ +Authorize.net,Authorize.net +"Gateway URL","Gateway URL" +"Invalid payload type.","Invalid payload type." +"Something went wrong in the payment gateway.","Something went wrong in the payment gateway." +"Merchant MD5 (deprecated","Merchant MD5 (deprecated" +"Signature Key","Signature Key" +"Basic Authorize.Net Settings","Basic Authorize.Net Settings" +"Advanced Authorie.Net Settings","Advanced Authorie.Net Settings" +"Public Client Key","Public Client Key" +"Environment","Environment" +"Production","Production" +"Sandbox","Sandbox" +"accountType","Account Type" +"authCode", "Processor Response Text" +"avsResultCode", "AVS Response Code" +"cvvResultCode","CVV Response Code" +"cavvResultCode","CAVV Response Code" +"Enable Credit Card Verification Field","Enable Credit Card Verification Field" +"ccLast4","Last 4 Digits of Card" +"There was an error while trying to process the refund.","There was an error while trying to process the refund." +"This transaction cannot be refunded with its current status.","This transaction cannot be refunded with its current status." diff --git a/app/code/Magento/AuthorizenetAcceptjs/registration.php b/app/code/Magento/AuthorizenetAcceptjs/registration.php new file mode 100644 index 0000000000000..5338c9a4ddc80 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/registration.php @@ -0,0 +1,11 @@ + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + Magento_AuthorizenetAcceptjs::form/cc.phtml + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml new file mode 100644 index 0000000000000..13f6d38e2b81a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml @@ -0,0 +1,17 @@ + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + Magento_AuthorizenetAcceptjs::form/cc.phtml + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/form/cc.phtml new file mode 100644 index 0000000000000..045bd5cfd81b2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/form/cc.phtml @@ -0,0 +1,93 @@ +escapeHtml($block->getMethodCode()); +$ccType = $block->getInfoData('cc_type'); +$ccExpMonth = $block->getInfoData('cc_exp_month'); +$ccExpYear = $block->getInfoData('cc_exp_year'); +?> + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/payment/script.phtml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/payment/script.phtml new file mode 100644 index 0000000000000..6960bddf696af --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/payment/script.phtml @@ -0,0 +1,17 @@ + + \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js new file mode 100644 index 0000000000000..0eb865d7666b3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js @@ -0,0 +1,196 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'uiComponent', + 'Magento_Ui/js/modal/alert', + 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-client' +], function ($, Class, alert, AcceptjsClient) { + 'use strict'; + + return Class.extend({ + defaults: { + acceptjsClient: null, + $selector: null, + selector: 'edit_form', + container: 'payment_form_authorizenet_acceptjs', + active: false, + imports: { + onActiveChange: 'active' + } + }, + + /** + * @{inheritdoc} + */ + initConfig: function (config) { + this._super(); + + this.acceptjsClient = AcceptjsClient({ + environment: config.environment + }); + + return this; + }, + + /** + * @{inheritdoc} + */ + initObservable: function () { + this.$selector = $('#' + this.selector); + this._super() + .observe('active'); + + // re-init payment method events + this.$selector.off('changePaymentMethod.' + this.code) + .on('changePaymentMethod.' + this.code, this.changePaymentMethod.bind(this)); + + return this; + }, + + /** + * Enable/disable current payment method + * + * @param {Object} event + * @param {String} method + * @returns {Object} + */ + changePaymentMethod: function (event, method) { + this.active(method === this.code); + + return this; + }, + + /** + * Triggered when payment changed + * + * @param {Boolean} isActive + */ + onActiveChange: function (isActive) { + if (!isActive) { + + return; + } + + this.disableEventListeners(); + + window.order.addExcludedPaymentMethod(this.code); + + this.enableEventListeners(); + }, + + /** + * Sets the payment details on the form + * + * @param {Object} tokens + */ + setPaymentDetails: function (tokens) { + var $ccNumber = $(this.getSelector('cc_number')), + ccLast4 = $ccNumber.val().replace(/[^\d]/g, '').substr(-4); + + $(this.getSelector('opaque_data_descriptor')).val(tokens.opaqueDataDescriptor); + $(this.getSelector('opaque_data_value')).val(tokens.opaqueDataValue); + $(this.getSelector('cc_last_4')).val(ccLast4); + $ccNumber.val(''); + $(this.getSelector('cc_exp_month')).val(''); + $(this.getSelector('cc_exp_year')).val(''); + + if (this.useCvv) { + $(this.getSelector('cc_cid')).val(''); + } + }, + + /** + * Trigger order submit + */ + submitOrder: function () { + var authData = {}, + cardData = {}, + secureData = {}; + + this.$selector.validate().form(); + this.$selector.trigger('afterValidate.beforeSubmit'); + + authData.clientKey = this.clientKey; + authData.apiLoginID = this.apiLoginID; + + cardData.cardNumber = $(this.getSelector('cc_number')).val(); + cardData.month = $(this.getSelector('cc_exp_month')).val(); + cardData.year = $(this.getSelector('cc_exp_year')).val(); + + if (this.useCvv) { + cardData.cardCode = $(this.getSelector('cc_cid')).val(); + } + + secureData.authData = authData; + secureData.cardData = cardData; + + this.disableEventListeners(); + + this.acceptjsClient.createTokens(secureData) + .always(function () { + $('body').trigger('processStop'); + this.enableEventListeners(); + }.bind(this)) + .done(function (tokens) { + this.setPaymentDetails(tokens); + this.placeOrder(); + }.bind(this)) + .fail(function (messages) { + this.tokens = null; + + if (messages.length > 0) { + this._showError(messages[0]); + } + }.bind(this)); + + return false; + }, + + /** + * Place order + */ + placeOrder: function () { + this.$selector.trigger('realOrder'); + }, + + /** + * Get jQuery selector + * + * @param {String} field + * @returns {String} + */ + getSelector: function (field) { + return '#' + this.code + '_' + field; + }, + + /** + * Show alert message + * + * @param {String} message + */ + _showError: function (message) { + alert({ + content: message + }); + }, + + /** + * Enable form event listeners + */ + enableEventListeners: function () { + this.$selector.on('submitOrder.authorizenetacceptjs', this.submitOrder.bind(this)); + }, + + /** + * Disable form event listeners + */ + disableEventListeners: function () { + this.$selector.off('submitOrder'); + this.$selector.off('submit'); + } + + }); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js new file mode 100644 index 0000000000000..68c2f22f6ed44 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_AuthorizenetAcceptjs/js/authorizenet', + 'jquery' +], function (AuthorizenetAcceptjs, $) { + 'use strict'; + + return function (data, element) { + var $form = $(element), + config = data.config; + + config.active = $form.length > 0 && !$form.is(':hidden'); + new AuthorizenetAcceptjs(config); + }; +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js new file mode 100644 index 0000000000000..83ddd1094ea1a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + shim: { + acceptjs: { + exports: 'Accept' + }, + acceptjssandbox: { + exports: 'Accept' + } + }, + paths: { + acceptjssandbox: 'https://jstest.authorize.net/v1/Accept', + acceptjs: 'https://js.authorize.net/v1/Accept' + } +}; diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js new file mode 100644 index 0000000000000..935465f5298eb --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js @@ -0,0 +1,73 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiClass', + 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-factory', + 'Magento_AuthorizenetAcceptjs/js/view/payment/validator-handler' +], function ($, Class, acceptjsFactory, validatorHandler) { + 'use strict'; + + return Class.extend({ + defaults: { + environment: 'production' + }, + + /** + * @{inheritdoc} + */ + initialize: function () { + validatorHandler.initialize(); + + this._super(); + + return this; + }, + + /** + * Creates the token pair with the provided data + * + * @param {Object} data + * @return {jQuery.Deferred} + */ + createTokens: function (data) { + var deferred = $.Deferred(); + + if (this.acceptjsClient) { + this._createTokens(deferred, data); + } else { + acceptjsFactory(this.environment) + .done(function (client) { + this.acceptjsClient = client; + this._createTokens(deferred, data); + }.bind(this)); + } + + return deferred.promise(); + }, + + /** + * Creates a token from the payment information in the form + * + * @param {jQuery.Deferred} deferred + * @param {Object} data + */ + _createTokens: function (deferred, data) { + this.acceptjsClient.dispatchData(data, function (response) { + validatorHandler.validate(response, function (valid, messages) { + if (valid) { + deferred.resolve({ + opaqueDataDescriptor: response.opaqueData.dataDescriptor, + opaqueDataValue: response.opaqueData.dataValue + }); + } else { + deferred.reject(messages); + } + }); + }); + } + }); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js new file mode 100644 index 0000000000000..e98a204e36cee --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js @@ -0,0 +1,38 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (environment) { + var deferred = $.Deferred(), + dependency = 'acceptjs'; + + if (environment === 'sandbox') { + dependency = 'acceptjssandbox'; + } + + require([dependency], function (accept) { + var $body = $('body'); + + /* + * Acceptjs doesn't safely load dependent files which leads to a race condition when trying to use + * the sdk right away. + * @see https://community.developer.authorize.net/t5/Integration-and-Testing/ + * Dynamically-loading-Accept-js-E-WC-03-Accept-js-is-not-loaded/td-p/63283 + */ + $body.on('handshake.acceptjs', function () { + deferred.resolve(accept); + $body.off('handshake.acceptjs'); + }); + }, + deferred.reject + ); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js new file mode 100644 index 0000000000000..3c44ca2f9e490 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js @@ -0,0 +1,38 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return { + /** + * Validate Authorizenet-Acceptjs response + * + * @param {Object} context + * @returns {jQuery.Deferred} + */ + validate: function (context) { + var state = $.Deferred(), + messages = []; + + if (context.messages.resultCode === 'Ok') { + state.resolve(); + } else { + if (context.messages.message.length > 0) { + $.each(context.messages.message, function (index, element) { + messages.push($t(element.text)); + }); + } + state.reject(messages); + } + + return state.promise(); + } + }; +}); + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js new file mode 100644 index 0000000000000..109f159c9a77c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js @@ -0,0 +1,59 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_AuthorizenetAcceptjs/js/view/payment/response-validator' +], function ($, responseValidator) { + 'use strict'; + + return { + validators: [], + + /** + * Init list of validators + */ + initialize: function () { + this.add(responseValidator); + }, + + /** + * Add new validator + * @param {Object} validator + */ + add: function (validator) { + this.validators.push(validator); + }, + + /** + * Run pull of validators + * @param {Object} context + * @param {Function} callback + */ + validate: function (context, callback) { + var self = this, + deferred; + + // no available validators + if (!self.validators.length) { + callback(true); + + return; + } + + // get list of deferred validators + deferred = $.map(self.validators, function (current) { + return current.validate(context); + }); + + $.when.apply($, deferred) + .done(function () { + callback(true); + }).fail(function (error) { + callback(false, error); + }); + } + }; +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 0000000000000..f31b06c9be9b9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + uiComponent + + + + + + + + Magento_AuthorizenetAcceptjs/js/view/payment/authorizenet + + + true + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js new file mode 100644 index 0000000000000..a05fe739a444a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Checkout/js/model/payment/renderer-list' +], +function (Component, rendererList) { + 'use strict'; + + rendererList.push({ + type: 'authorizenet_acceptjs', + component: 'Magento_AuthorizenetAcceptjs/js/view/payment/method-renderer/authorizenet-accept' + }); + + /** Add view logic here if needed */ + return Component.extend({}); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js new file mode 100644 index 0000000000000..983318c4cdaaf --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js @@ -0,0 +1,146 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Payment/js/view/payment/cc-form', + 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-client', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Ui/js/model/messageList', + 'Magento_Payment/js/model/credit-card-validation/validator' +], function ($, Component, AcceptjsClient, fullScreenLoader, globalMessageList) { + 'use strict'; + + return Component.extend({ + defaults: { + active: false, + template: 'Magento_AuthorizenetAcceptjs/payment/authorizenet-acceptjs', + tokens: null, + ccForm: 'Magento_Payment/payment/cc-form', + acceptjsClient: null + }, + + /** + * Set list of observable attributes + * + * @returns {exports.initObservable} + */ + initObservable: function () { + this._super() + .observe(['active']); + + return this; + }, + + /** + * @returns {String} + */ + getCode: function () { + return 'authorizenet_acceptjs'; + }, + + /** + * Initialize form elements for validation + */ + initFormElement: function (element) { + this.formElement = element; + this.acceptjsClient = AcceptjsClient({ + environment: window.checkoutConfig.payment[this.getCode()].environment + }); + $(this.formElement).validation(); + }, + + /** + * @returns {Object} + */ + getData: function () { + return { + method: this.getCode(), + 'additional_data': { + opaqueDataDescriptor: this.tokens ? this.tokens.opaqueDataDescriptor : null, + opaqueDataValue: this.tokens ? this.tokens.opaqueDataValue : null, + ccLast4: this.creditCardNumber().substr(-4) + } + }; + }, + + /** + * Check if payment is active + * + * @returns {Boolean} + */ + isActive: function () { + var active = this.getCode() === this.isChecked(); + + this.active(active); + + return active; + }, + + /** + * Prepare data to place order + */ + beforePlaceOrder: function () { + var authData = {}, + cardData = {}, + secureData = {}; + + if (!$(this.formElement).valid()) { + return; + } + + authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey; + authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID; + + cardData.cardNumber = this.creditCardNumber(); + cardData.month = this.creditCardExpMonth(); + cardData.year = this.creditCardExpYear(); + + if (this.hasVerification()) { + cardData.cardCode = this.creditCardVerificationNumber(); + } + + secureData.authData = authData; + secureData.cardData = cardData; + + fullScreenLoader.startLoader(); + + this.acceptjsClient.createTokens(secureData) + .always(function () { + fullScreenLoader.stopLoader(); + }) + .done(function (tokens) { + this.tokens = tokens; + this.placeOrder(); + }.bind(this)) + .fail(function (messages) { + this.tokens = null; + this._showErrors(messages); + }.bind(this)); + }, + + /** + * Should the cvv field be used + * + * @return {Boolean} + */ + hasVerification: function () { + return window.checkoutConfig.payment[this.getCode()].useCvv; + }, + + /** + * Show error messages + * + * @param {String[]} errorMessages + */ + _showErrors: function (errorMessages) { + $.each(errorMessages, function (index, message) { + globalMessageList.addErrorMessage({ + message: message + }); + }); + } + }); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html new file mode 100644 index 0000000000000..6db52a2b1025e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html @@ -0,0 +1,45 @@ + +
+
+ + +
+
+ +
+ +
+
+ + +
+ +
+
+
+ +
+
+
+
diff --git a/app/code/Magento/Backend/App/Request/BackendValidator.php b/app/code/Magento/Backend/App/Request/BackendValidator.php index 4d04d2fed8eb2..7ec6c83f29b50 100644 --- a/app/code/Magento/Backend/App/Request/BackendValidator.php +++ b/app/code/Magento/Backend/App/Request/BackendValidator.php @@ -146,8 +146,9 @@ private function createException( $exception = new InvalidRequestException($response); } else { //For regular requests. + $startPageUrl = $this->backendUrl->getStartupPageUrl(); $response = $this->redirectFactory->create() - ->setUrl($this->backendUrl->getStartupPageUrl()); + ->setUrl($this->backendUrl->getUrl($startPageUrl)); $exception = new InvalidRequestException( $response, [ diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index 8e238ccab44cb..b76421e4e6f67 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Block\Dashboard; /** @@ -15,7 +17,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Api URL */ - const API_URL = 'http://chart.apis.google.com/chart'; + const API_URL = 'https://image-charts.com/chart'; /** * All series @@ -76,6 +78,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Google chart api data encoding * + * @deprecated since the Google Image Charts API not accessible from March 14, 2019 * @var string */ protected $_encoding = 'e'; @@ -111,8 +114,8 @@ public function __construct( \Magento\Backend\Helper\Dashboard\Data $dashboardData, array $data = [] ) { - $this->_dashboardData = $dashboardData; parent::__construct($context, $collectionFactory, $data); + $this->_dashboardData = $dashboardData; } /** @@ -128,7 +131,7 @@ protected function _getTabTemplate() /** * Set data rows * - * @param array $rows + * @param string $rows * @return void */ public function setDataRows($rows) @@ -152,15 +155,14 @@ public function addSeries($seriesId, array $options) * Get series * * @param string $seriesId - * @return array|false + * @return array|bool */ public function getSeries($seriesId) { if (isset($this->_allSeries[$seriesId])) { return $this->_allSeries[$seriesId]; - } else { - return false; } + return false; } /** @@ -187,11 +189,12 @@ public function getChartUrl($directUrl = true) { $params = [ 'cht' => 'lc', - 'chf' => 'bg,s,ffffff', - 'chco' => 'ef672f', 'chls' => '7', - 'chxs' => '0,676056,15,0,l,676056|1,676056,15,0,l,676056', - 'chm' => 'h,f2ebde,0,0:1:.1,1,-1', + 'chf' => 'bg,s,f4f4f4|c,lg,90,ffffff,0.1,ededed,0', + 'chm' => 'B,f4d4b2,0,0,0', + 'chco' => 'db4814', + 'chxs' => '0,0,11|1,0,11', + 'chma' => '15,15,15,15' ]; $this->_allSeries = $this->getRowsData($this->_dataRows); @@ -279,20 +282,11 @@ public function getChartUrl($directUrl = true) $this->_axisLabels['x'] = $dates; $this->_allSeries = $datas; - //Google encoding values - if ($this->_encoding == "s") { - // simple encoding - $params['chd'] = "s:"; - $dataDelimiter = ""; - $dataSetdelimiter = ","; - $dataMissing = "_"; - } else { - // extended encoding - $params['chd'] = "e:"; - $dataDelimiter = ""; - $dataSetdelimiter = ","; - $dataMissing = "__"; - } + // Image-Charts Awesome data format values + $params['chd'] = "a:"; + $dataDelimiter = ","; + $dataSetdelimiter = "|"; + $dataMissing = "_"; // process each string in the array, and find the max length $localmaxvalue = [0]; @@ -306,7 +300,6 @@ public function getChartUrl($directUrl = true) $minvalue = min($localminvalue); // default values - $yrange = 0; $yLabels = []; $miny = 0; $maxy = 0; @@ -314,14 +307,13 @@ public function getChartUrl($directUrl = true) if ($minvalue >= 0 && $maxvalue >= 0) { if ($maxvalue > 10) { - $p = pow(10, $this->_getPow($maxvalue)); + $p = pow(10, $this->_getPow((int) $maxvalue)); $maxy = ceil($maxvalue / $p) * $p; $yLabels = range($miny, $maxy, $p); } else { $maxy = ceil($maxvalue + 1); $yLabels = range($miny, $maxy, 1); } - $yrange = $maxy; $yorigin = 0; } @@ -329,44 +321,14 @@ public function getChartUrl($directUrl = true) foreach ($this->getAllSeries() as $index => $serie) { $thisdataarray = $serie; - if ($this->_encoding == "s") { - // SIMPLE ENCODING - for ($j = 0; $j < sizeof($thisdataarray); $j++) { - $currentvalue = $thisdataarray[$j]; - if (is_numeric($currentvalue)) { - $ylocation = round( - (strlen($this->_simpleEncoding) - 1) * ($yorigin + $currentvalue) / $yrange - ); - $chartdata[] = substr($this->_simpleEncoding, $ylocation, 1) . $dataDelimiter; - } else { - $chartdata[] = $dataMissing . $dataDelimiter; - } - } - } else { - // EXTENDED ENCODING - for ($j = 0; $j < sizeof($thisdataarray); $j++) { - $currentvalue = $thisdataarray[$j]; - if (is_numeric($currentvalue)) { - if ($yrange) { - $ylocation = 4095 * ($yorigin + $currentvalue) / $yrange; - } else { - $ylocation = 0; - } - $firstchar = floor($ylocation / 64); - $secondchar = $ylocation % 64; - $mappedchar = substr( - $this->_extendedEncoding, - $firstchar, - 1 - ) . substr( - $this->_extendedEncoding, - $secondchar, - 1 - ); - $chartdata[] = $mappedchar . $dataDelimiter; - } else { - $chartdata[] = $dataMissing . $dataDelimiter; - } + $count = count($thisdataarray); + for ($j = 0; $j < $count; $j++) { + $currentvalue = $thisdataarray[$j]; + if (is_numeric($currentvalue)) { + $ylocation = $yorigin + $currentvalue; + $chartdata[] = $ylocation . $dataDelimiter; + } else { + $chartdata[] = $dataMissing . $dataDelimiter; } } $chartdata[] = $dataSetdelimiter; @@ -381,45 +343,13 @@ public function getChartUrl($directUrl = true) $valueBuffer = []; - if (sizeof($this->_axisLabels) > 0) { + if (count($this->_axisLabels) > 0) { $params['chxt'] = implode(',', array_keys($this->_axisLabels)); $indexid = 0; foreach ($this->_axisLabels as $idx => $labels) { if ($idx == 'x') { - /** - * Format date - */ - foreach ($this->_axisLabels[$idx] as $_index => $_label) { - if ($_label != '') { - $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); - switch ($this->getDataHelper()->getParam('period')) { - case '24h': - $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( - $period->setTime($period->format('H'), 0, 0), - \IntlDateFormatter::NONE, - \IntlDateFormatter::SHORT - ); - break; - case '7d': - case '1m': - $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( - $period, - \IntlDateFormatter::SHORT, - \IntlDateFormatter::NONE - ); - break; - case '1y': - case '2y': - $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); - break; - } - } else { - $this->_axisLabels[$idx][$_index] = ''; - } - } - + $this->formatAxisLabelDate((string) $idx, (string) $timezoneLocal); $tmpstring = implode('|', $this->_axisLabels[$idx]); - $valueBuffer[] = $indexid . ":|" . $tmpstring; } elseif ($idx == 'y') { $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels); @@ -438,12 +368,51 @@ public function getChartUrl($directUrl = true) foreach ($params as $name => $value) { $p[] = $name . '=' . urlencode($value); } - return self::API_URL . '?' . implode('&', $p); - } else { - $gaData = urlencode(base64_encode(json_encode($params))); - $gaHash = $this->_dashboardData->getChartDataHash($gaData); - $params = ['ga' => $gaData, 'h' => $gaHash]; - return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); + return (string) self::API_URL . '?' . implode('&', $p); + } + $gaData = urlencode(base64_encode(json_encode($params))); + $gaHash = $this->_dashboardData->getChartDataHash($gaData); + $params = ['ga' => $gaData, 'h' => $gaHash]; + return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); + } + + /** + * Format dates for axis labels + * + * @param string $idx + * @param string $timezoneLocal + * + * @return void + */ + private function formatAxisLabelDate($idx, $timezoneLocal) + { + foreach ($this->_axisLabels[$idx] as $_index => $_label) { + if ($_label != '') { + $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); + switch ($this->getDataHelper()->getParam('period')) { + case '24h': + $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( + $period->setTime((int) $period->format('H'), 0, 0), + \IntlDateFormatter::NONE, + \IntlDateFormatter::SHORT + ); + break; + case '7d': + case '1m': + $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( + $period, + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE + ); + break; + case '1y': + case '2y': + $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); + break; + } + } else { + $this->_axisLabels[$idx][$_index] = ''; + } } } @@ -540,6 +509,8 @@ protected function getHeight() } /** + * Sets data helper + * * @param \Magento\Backend\Helper\Dashboard\AbstractDashboard $dataHelper * @return void */ diff --git a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php index 5a09e1f17f617..6004d08b4b738 100644 --- a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php +++ b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php @@ -6,6 +6,9 @@ namespace Magento\Backend\Block\System\Design\Edit\Tab; +/** + * General system tab block. + */ class General extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -90,7 +93,7 @@ protected function _prepareForm() ] ); - $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); $fieldset->addField( 'date_from', 'date', diff --git a/app/code/Magento/Backend/Block/Template/Context.php b/app/code/Magento/Backend/Block/Template/Context.php index 6efc8d86802ce..27c777c6d4009 100644 --- a/app/code/Magento/Backend/Block/Template/Context.php +++ b/app/code/Magento/Backend/Block/Template/Context.php @@ -17,7 +17,9 @@ * the classes they were introduced for. * * @api + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Context extends \Magento\Framework\View\Element\Template\Context @@ -173,6 +175,8 @@ public function getAuthorization() } /** + * Get backend session instance. + * * @return \Magento\Backend\Model\Session */ public function getBackendSession() @@ -181,6 +185,8 @@ public function getBackendSession() } /** + * Get math random instance. + * * @return \Magento\Framework\Math\Random */ public function getMathRandom() @@ -189,6 +195,8 @@ public function getMathRandom() } /** + * Get form key instance. + * * @return \Magento\Framework\Data\Form\FormKey */ public function getFormKey() @@ -197,7 +205,9 @@ public function getFormKey() } /** - * @return \Magento\Framework\Data\Form\FormKey + * Get name builder instance. + * + * @return \Magento\Framework\Code\NameBuilder */ public function getNameBuilder() { diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php index 97116de6db79b..febaae3861688 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Container.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php @@ -56,6 +56,8 @@ class Container extends \Magento\Backend\Block\Widget\Container protected $_template = 'Magento_Backend::widget/form/container.phtml'; /** + * Initialize form. + * * @return void */ protected function _construct() @@ -83,7 +85,7 @@ protected function _construct() -1 ); - $objId = $this->getRequest()->getParam($this->_objectId); + $objId = (int)$this->getRequest()->getParam($this->_objectId); if (!empty($objId)) { $this->addButton( @@ -151,11 +153,13 @@ public function getBackUrl() } /** + * Get URL for delete button. + * * @return string */ public function getDeleteUrl() { - return $this->getUrl('*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId)]); + return $this->getUrl('*/*/delete', [$this->_objectId => (int)$this->getRequest()->getParam($this->_objectId)]); } /** @@ -183,6 +187,8 @@ public function getFormActionUrl() } /** + * Get form HTML. + * * @return string */ public function getFormHtml() @@ -192,6 +198,8 @@ public function getFormHtml() } /** + * Get form init scripts. + * * @return string */ public function getFormInitScripts() @@ -203,6 +211,8 @@ public function getFormInitScripts() } /** + * Get form scripts. + * * @return string */ public function getFormScripts() @@ -214,6 +224,8 @@ public function getFormScripts() } /** + * Get header width. + * * @return string */ public function getHeaderWidth() @@ -222,6 +234,8 @@ public function getHeaderWidth() } /** + * Get header css class. + * * @return string */ public function getHeaderCssClass() @@ -230,6 +244,8 @@ public function getHeaderCssClass() } /** + * Get header HTML. + * * @return string */ public function getHeaderHtml() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php index b8a2e283b29a0..623a75015eb2f 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php @@ -28,6 +28,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp protected $_column; /** + * Set column for renderer. + * * @param Column $column * @return $this */ @@ -38,6 +40,8 @@ public function setColumn($column) } /** + * Returns row associated with the renderer. + * * @return Column */ public function getColumn() @@ -48,7 +52,7 @@ public function getColumn() /** * Renders grid column * - * @param Object $row + * @param DataObject $row * @return string */ public function render(DataObject $row) @@ -66,7 +70,7 @@ public function render(DataObject $row) /** * Render column for export * - * @param Object $row + * @param DataObject $row * @return string */ public function renderExport(DataObject $row) @@ -75,7 +79,9 @@ public function renderExport(DataObject $row) } /** - * @param Object $row + * Returns value of the row. + * + * @param DataObject $row * @return mixed */ protected function _getValue(DataObject $row) @@ -92,7 +98,9 @@ protected function _getValue(DataObject $row) } /** - * @param Object $row + * Get pre-rendered input element. + * + * @param DataObject $row * @return string */ public function _getInputValueElement(DataObject $row) @@ -108,7 +116,9 @@ public function _getInputValueElement(DataObject $row) } /** - * @param Object $row + * Get input value by row. + * + * @param DataObject $row * @return mixed */ protected function _getInputValue(DataObject $row) @@ -117,6 +127,8 @@ protected function _getInputValue(DataObject $row) } /** + * Renders header of the column, + * * @return string */ public function renderHeader() @@ -148,6 +160,8 @@ public function renderHeader() } /** + * Render HTML properties. + * * @return string */ public function renderProperty() @@ -172,6 +186,8 @@ public function renderProperty() } /** + * Returns HTML for CSS. + * * @return string */ public function renderCss() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 9890a10a4ceb0..891b2a3ada724 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -282,25 +282,23 @@ public function getGridIdsJson() if (!$this->getUseSelectAll()) { return ''; } - /** @var \Magento\Framework\Data\Collection $allIdsCollection */ - $allIdsCollection = clone $this->getParentBlock()->getCollection(); - if ($this->getMassactionIdField()) { - $massActionIdField = $this->getMassactionIdField(); + /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ + $collection = clone $this->getParentBlock()->getCollection(); + + if ($collection instanceof AbstractDb) { + $idsSelect = clone $collection->getSelect(); + $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); + $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $idsSelect->columns($this->getMassactionIdField(), 'main_table'); + $idList = $collection->getConnection()->fetchCol($idsSelect); } else { - $massActionIdField = $this->getParentBlock()->getMassactionIdField(); + $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); } - if ($allIdsCollection instanceof AbstractDb) { - $allIdsCollection->getSelect()->limit(); - $allIdsCollection->clear(); - } - - $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); - if (!empty($gridIds)) { - return join(",", $gridIds); - } - return ''; + return implode(',', $idList); } /** diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php index 3907f4a4f71a2..a42a44814cb0c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php @@ -6,12 +6,17 @@ */ namespace Magento\Backend\Controller\Adminhtml\Dashboard; -class ProductsViewed extends AjaxBlock +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Get most viewed products controller. + */ +class ProductsViewed extends AjaxBlock implements HttpPostActionInterface { /** * Gets most viewed products list * - * @return \Magento\Backend\Model\View\Result\Page + * @return \Magento\Framework\Controller\Result\Raw */ public function execute() { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php index c10d1a77997b7..c709859adb190 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php @@ -6,7 +6,13 @@ namespace Magento\Backend\Controller\Adminhtml\Dashboard; -class RefreshStatistics extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Reports\Controller\Adminhtml\Report\Statistics; + +/** + * Refresh Dashboard statistics action. + */ +class RefreshStatistics extends Statistics implements HttpPostActionInterface { /** * @param \Magento\Backend\App\Action\Context $context @@ -25,6 +31,8 @@ public function __construct( } /** + * Refresh statistics. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php index 0228b48f7f11e..25cfb61d658c3 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php @@ -6,7 +6,12 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Design; -class Save extends \Magento\Backend\Controller\Adminhtml\System\Design +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Save design action. + */ +class Save extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpPostActionInterface { /** * Filtering posted data. Converting localized data if needed @@ -26,6 +31,8 @@ protected function _filterPostData($data) } /** + * Save design action. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -54,10 +61,10 @@ public function execute() } catch (\Exception $e) { $this->messageManager->addErrorMessage($e->getMessage()); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setDesignData($data); - return $resultRedirect->setPath('adminhtml/*/', ['id' => $design->getId()]); + return $resultRedirect->setPath('*/*/edit', ['id' => $design->getId()]); } } - return $resultRedirect->setPath('adminhtml/*/'); + return $resultRedirect->setPath('*/*/'); } } diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php index 11edaa26f443f..ed0312874565c 100644 --- a/app/code/Magento/Backend/Model/Session/Quote.php +++ b/app/code/Magento/Backend/Model/Session/Quote.php @@ -24,6 +24,7 @@ * @method Quote setOrderId($orderId) * @method int getOrderId() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Quote extends \Magento\Framework\Session\SessionManager @@ -149,7 +150,8 @@ public function getQuote() $this->_quote = $this->quoteFactory->create(); if ($this->getStoreId()) { if (!$this->getQuoteId()) { - $this->_quote->setCustomerGroupId($this->groupManagement->getDefaultGroup()->getId()); + $customerGroupId = $this->groupManagement->getDefaultGroup($this->getStoreId())->getId(); + $this->_quote->setCustomerGroupId($customerGroupId); $this->_quote->setIsActive(false); $this->_quote->setStoreId($this->getStoreId()); diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminAssertPageTitleActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminAssertPageTitleActionGroup.xml new file mode 100644 index 0000000000000..42ffb4b7421ac --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminAssertPageTitleActionGroup.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml new file mode 100644 index 0000000000000..8e0f5a067610d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml new file mode 100644 index 0000000000000..1c86a736ac2f1 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml new file mode 100644 index 0000000000000..844f58c789a15 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml new file mode 100644 index 0000000000000..607fba3736c42 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml new file mode 100644 index 0000000000000..6aaa612b249b6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml index 1070bc409962a..b2fbadcbe38e2 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml @@ -10,11 +10,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - + - - + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml new file mode 100644 index 0000000000000..9e5c0bb3f39bf --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml new file mode 100644 index 0000000000000..4519648eb1d1b --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..4fe600d194e61 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,46 @@ + + + + + + Content + Content + magento-backend-content + + + Store Design Schedule + Schedule + magento-backend-system-design-schedule + + + Dashboard + Dashboard + magento-backend-dashboard + + + Stores + Stores + magento-backend-stores + + + Stores + All Stores + magento-backend-system-store + + + Configuration + Configuration + magento-config-system-config + + + Cache Management + Cache Management + magento-backend-system-cache + + diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml index 8c258accdf06c..ed30395406f7d 100644 --- a/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd">
diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml new file mode 100644 index 0000000000000..84af56d102d84 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml index b68b9914186f6..78226d79273d9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml @@ -9,6 +9,7 @@ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml new file mode 100644 index 0000000000000..2f04c2c11d288 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml new file mode 100644 index 0000000000000..664c335a4cfc6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml new file mode 100644 index 0000000000000..efaca22123354 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index 441ce886f117b..5b517c7be8a79 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml index 3b10fac7bb9dc..bd65dea89abc2 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml @@ -12,5 +12,6 @@ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml new file mode 100644 index 0000000000000..f6ada50ada357 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml index ef2e5f4216889..4867b5ba5ae08 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml @@ -12,5 +12,6 @@ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml index 9e4a6d9219526..8498ad8c52e41 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml @@ -16,5 +16,13 @@ + + + + + + + +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml new file mode 100644 index 0000000000000..b9570ce945943 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml new file mode 100644 index 0000000000000..2e2e5aec35ecd --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml @@ -0,0 +1,18 @@ + + + + +
+ + + + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml index c50bf0664f9cb..a460aaebf1051 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -11,5 +11,6 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml new file mode 100644 index 0000000000000..2c061e54f5509 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + <description value="Check that attribute text swatches can be filed"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96710"/> + <useCaseId value="MAGETWO-96409"/> + <group value="backend"/> + <group value="ui"/> + </annotations> + <before> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + </before> + <after> + <!-- Delete all 10 store views --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView3"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView4"> + <argument name="customStore" value="storeViewData1"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView5"> + <argument name="customStore" value="storeViewData2"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView6"> + <argument name="customStore" value="storeViewData3"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView7"> + <argument name="customStore" value="storeViewData4"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView8"> + <argument name="customStore" value="storeViewData5"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView9"> + <argument name="customStore" value="storeViewData6"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView10"> + <argument name="customStore" value="storeViewData7"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create 10 store views --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView3"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView4"> + <argument name="customStore" value="storeViewData1"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView5"> + <argument name="customStore" value="storeViewData2"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView6"> + <argument name="customStore" value="storeViewData3"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView7"> + <argument name="customStore" value="storeViewData4"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView8"> + <argument name="customStore" value="storeViewData5"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView9"> + <argument name="customStore" value="storeViewData6"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView10"> + <argument name="customStore" value="storeViewData7"/> + </actionGroup> + + <!--Navigate to Product attribute page--> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField userInput="test_label" selector="{{AttributePropertiesSection.DefaultLabel}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Swatch" stepKey="selectInputType"/> + <click selector="{{AttributePropertiesSection.addSwatch}}" stepKey="clickAddSwatch"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + + <!-- Fill Swatch and Description fields for Admin --> + <fillField selector="{{AttributeManageSwatchSection.swatchField('Admin')}}" userInput="test" stepKey="fillSwatchForAdmin"/> + <fillField selector="{{AttributeManageSwatchSection.descriptionField('Admin')}}" userInput="test" stepKey="fillDescriptionForAdmin"/> + + <!-- Grab value Swatch and Description fields for Admin --> + <grabValueFrom selector="{{AttributeManageSwatchSection.swatchField('Admin')}}" stepKey="grabSwatchForAdmin"/> + <grabValueFrom selector="{{AttributeManageSwatchSection.descriptionField('Admin')}}" stepKey="grabDescriptionForAdmin"/> + + <!-- Check that Swatch and Description fields for Admin are not empty--> + <assertNotEmpty actual="$grabSwatchForAdmin" stepKey="checkSwatchFieldForAdmin"/> + <assertNotEmpty actual="$grabDescriptionForAdmin" stepKey="checkDescriptionFieldForAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml new file mode 100644 index 0000000000000..bead59653eee8 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminContentScheduleNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin content schedule navigate menu test"/> + <description value="Admin should be able to navigate to Content > Schedule"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14117"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentSchedulePage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentDesignSchedule.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentDesignSchedule.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml new file mode 100644 index 0000000000000..33561d7c3b03e --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml @@ -0,0 +1,33 @@ +<?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="AdminDashboardNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin dashboard navigate menu test"/> + <description value="Admin should be able to navigate to Dashboard"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14116"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <click selector="{{AdminMenuSection.menuItem(AdminMenuDashboard.dataUiId)}}" stepKey="clickOnMenuItem"/> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuDashboard.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml new file mode 100644 index 0000000000000..55cb5a71505a5 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.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="AdminDashboardWithChartsTest"> + <annotations> + <features value="Backend"/> + <title value="Google chart on Magento dashboard"/> + <description value="Google chart on Magento dashboard page is not broken"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98934"/> + <useCaseId value="MAGETWO-98584"/> + <group value="backend"/> + </annotations> + <before> + <magentoCLI command="config:set admin/dashboard/enable_charts 1" stepKey="setEnableCharts" /> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">150</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!-- Reset admin order filter --> + <comment userInput="Reset admin order filter" stepKey="resetAdminOrderFilter"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingOrderGrid"/> + <magentoCLI command="config:set admin/dashboard/enable_charts 0" stepKey="setDisableChartsAsDefault" /> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Login as admin --> + <comment userInput="Login as admin" stepKey="adminLogin"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Grab quantity value --> + <comment userInput="Grab quantity value from dashboard" stepKey="grabQuantityFromDashboard"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabStartQuantity"/> + <!-- Login as customer --> + <comment userInput="Login as customer" stepKey="loginAsCustomer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Add Product to Shopping Cart--> + <comment userInput="Add product to the shopping cart" stepKey="addProductToCart"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!--Go to Checkout--> + <comment userInput="Go to checkout" stepKey="goToCheckout"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingCheckoutPageWithShippingMethod"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <comment userInput="Select Check/Money payment" stepKey="checkoutSelectCheckMoneyPayment"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <!-- Place Order --> + <comment userInput="Place order" stepKey="placeOrder"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order number is: " stepKey="seeOrderNumber"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!-- Search for Order in the order grid --> + <comment userInput="Search for Order in the order grid" stepKey="searchOrderInGrid"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForSearchingOrder"/> + <!-- Create invoice --> + <comment userInput="Create invoice" stepKey="createInvoice"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> + <see selector="{{AdminInvoiceTotalSection.total('Subtotal')}}" userInput="$150.00" stepKey="seeCorrectGrandTotal"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessInvoiceMessage"/> + <!--Create Shipment for the order--> + <comment userInput="Create Shipment for the order" stepKey="createShipmentForOrder"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage2"/> + <waitForPageLoad time="30" stepKey="waitForOrderListPageLoading"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="openOrderPageForShip"/> + <waitForPageLoad stepKey="waitForOrderDetailsPage"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <waitForPageLoad stepKey="waitForShipmentPagePage"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> + <!--Submit Shipment--> + <comment userInput="Submit Shipment" stepKey="submitShipment"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <waitForPageLoad stepKey="waitForShipmentSubmit"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> + <!-- Go to dashboard page --> + <comment userInput="Go to dashboard page" stepKey="goToDashboardPage"/> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageLoad4"/> + <!-- Grab quantity value --> + <comment userInput="Grab quantity value from dashboard at the end" stepKey="grabQuantityFromDashboardAtTheEnd"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabEndQuantity"/> + <!-- Assert that page is not broken --> + <comment userInput="Assert that dashboard page is not broken" stepKey="assertDashboardPageIsNotBroken"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramOrderContentTab}}" stepKey="seeOrderContentTab"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramContent}}" stepKey="seeDiagramContent"/> + <click selector="{{AdminDashboardSection.dashboardDiagramAmounts}}" stepKey="clickDashboardAmount"/> + <waitForLoadingMaskToDisappear stepKey="waitForDashboardAmountLoading"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramAmountsContentTab}}" stepKey="seeDiagramAmountContent"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramTotals}}" stepKey="seeAmountTotals"/> + <dontSeeJsError stepKey="dontSeeJsError"/> + <assertGreaterThan expected="$grabStartQuantity" actual="$grabEndQuantity" stepKey="checkQuantityWasChanged"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml new file mode 100644 index 0000000000000..7758b387e393b --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresAllStoresNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin stores all stores navigate menu test"/> + <description value="Admin should be able to navigate to Stores > All Stores"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14118"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresAllStoresPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsAllStores.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsAllStores.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml new file mode 100644 index 0000000000000..a54269b186ba0 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresConfigurationNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin stores configuration navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Configuration"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14119"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresConfigurationPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsConfiguration.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsConfiguration.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml new file mode 100644 index 0000000000000..516631c1bd166 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemCacheManagementNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin system cache management navigate menu test"/> + <description value="Admin should be able to navigate to System > Cache Management"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14120"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemCacheManagementPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemToolsCacheManagement.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemToolsCacheManagement.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml new file mode 100644 index 0000000000000..5485dcaea33ee --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml @@ -0,0 +1,30 @@ +<?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="AdminUserLoginWithStoreCodeInUrlTest"> + <annotations> + <features value="Backend"/> + <title value="Admin panel should be accessible with Add Store Code to URL setting enabled"/> + <description value="Admin panel should be accessible with Add Store Code to URL setting enabled"/> + <testCaseId value="MC-14279" /> + <group value="backend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php index e8143b5f6b43a..e62b73f39241d 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php @@ -269,62 +269,6 @@ public function testGetGridIdsJsonWithoutUseSelectAll() $this->assertEmpty($this->_block->getGridIdsJson()); } - /** - * @param array $items - * @param string $result - * - * @dataProvider dataProviderGetGridIdsJsonWithUseSelectAll - */ - public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) - { - $this->_block->setUseSelectAll(true); - - if ($this->_block->getMassactionIdField()) { - $massActionIdField = $this->_block->getMassactionIdField(); - } else { - $massActionIdField = $this->_block->getParentBlock()->getMassactionIdField(); - } - - $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->_gridMock->expects($this->once()) - ->method('getCollection') - ->willReturn($collectionMock); - $collectionMock->expects($this->once()) - ->method('setPageSize') - ->with(0) - ->willReturnSelf(); - $collectionMock->expects($this->once()) - ->method('getColumnValues') - ->with($massActionIdField) - ->willReturn($items); - - $this->assertEquals($result, $this->_block->getGridIdsJson()); - } - - /** - * @return array - */ - public function dataProviderGetGridIdsJsonWithUseSelectAll() - { - return [ - [ - [], - '', - ], - [ - [1], - '1', - ], - [ - [1, 2, 3], - '1,2,3', - ], - ]; - } - /** * @param string $itemId * @param array|\Magento\Framework\DataObject $item diff --git a/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php b/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php index 869d4ba3f45b1..d159225089afc 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php @@ -267,7 +267,10 @@ public function testGetQuoteWithoutQuoteId() $cartInterfaceMock->expects($this->atLeastOnce())->method('getId')->willReturn($quoteId); $defaultGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class)->getMock(); $defaultGroup->expects($this->any())->method('getId')->will($this->returnValue($customerGroupId)); - $this->groupManagementMock->expects($this->any())->method('getDefaultGroup')->willReturn($defaultGroup); + $this->groupManagementMock + ->method('getDefaultGroup') + ->with($storeId) + ->willReturn($defaultGroup); $dataCustomerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 0fb7d89f924de..98b8e702b1c53 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -112,7 +112,7 @@ <group id="debug" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Debug</label> <field id="template_hints_storefront" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Enabled Template Path Hints for Storefront</label> + <label>Enable Template Path Hints for Storefront</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="template_hints_storefront_show_with_parameter" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -132,7 +132,7 @@ <comment>Add the following parameter to the URL to show template hints ?templatehints=[parameter_value]</comment> </field> <field id="template_hints_admin" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Enabled Template Path Hints for Admin</label> + <label>Enable Template Path Hints for Admin</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="template_hints_blocks" translate="label" type="select" sortOrder="21" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -169,15 +169,15 @@ </group> <group id="js" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> <label>JavaScript Settings</label> - <field id="merge_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="merge_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Merge JavaScript Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="enable_js_bundling" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="enable_js_bundling" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enable JavaScript Bundling</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minify JavaScript Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Minification is not applied in developer mode.</comment> @@ -185,11 +185,11 @@ </group> <group id="css" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="1"> <label>CSS Settings</label> - <field id="merge_css_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="merge_css_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Merge CSS Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minify CSS Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Minification is not applied in developer mode.</comment> diff --git a/app/code/Magento/Backend/etc/module.xml b/app/code/Magento/Backend/etc/module.xml index 3a5cd8226753d..03976396f6fd5 100644 --- a/app/code/Magento/Backend/etc/module.xml +++ b/app/code/Magento/Backend/etc/module.xml @@ -9,6 +9,7 @@ <module name="Magento_Backend"> <sequence> <module name="Magento_Directory"/> + <module name="Magento_Theme"/> </sequence> </module> </config> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml index 69d545f12d075..0a1dcb0b626e6 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml @@ -8,7 +8,7 @@ ?> <?php if ($block->getChildHtml()):?> - <div data-mage-init='{"floatingHeader": {}}' class="page-actions" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> + <div data-mage-init='{"floatingHeader": {}}' class="page-actions floating-header" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> <?= $block->getChildHtml() ?> </div> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml index 062528e742201..c76f10da0f927 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml @@ -18,7 +18,7 @@ <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> <li> - <a href="<?= /* @escapeNotVerified */ $_tabHref ?>" id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>" title="<?= /* @escapeNotVerified */ $block->getTabTitle($_tab) ?>" class="<?php $_tabClass ?>" data-tab-type="<?php $_tabType ?>"> + <a href="<?= $block->escapeHtmlAttr($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="<?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>"> <span> <span class="changed" title="<?= /* @escapeNotVerified */ __('The information in this tab has been changed.') ?>"></span> <span class="error" title="<?= /* @escapeNotVerified */ __('This tab contains invalid data. Please resolve this before saving.') ?>"></span> diff --git a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml index 93309c9a22ef2..b0abec3aa9bec 100644 --- a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml +++ b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml @@ -6,6 +6,7 @@ */ --> <listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top" /> <columns name="design_config_columns"> <column name="theme_theme_id" component="Magento_Ui/js/grid/columns/select" sortOrder="40"> <settings> diff --git a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html index fe30ca7e83f19..0033f4c071e42 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html +++ b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html @@ -69,7 +69,7 @@ <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> <td if="elem.template" - visible="elem.visible" + visible="elem.visible() && elem.formElement !== 'hidden'" disable="elem.disabled" css="$parent.setClasses(elem)" template="elem.template" diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php index 0edeb5565f288..b62963947d7bf 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php @@ -6,7 +6,6 @@ namespace Magento\Backup\Controller\Adminhtml; use Magento\Backend\App\Action; -use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backup\Helper\Data as Helper; use Magento\Framework\App\ObjectManager; @@ -18,7 +17,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.AllPurposeAction) */ -abstract class Index extends Action implements HttpGetActionInterface +abstract class Index extends Action { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php index 53f45aff50cbc..ee5a56e814837 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php @@ -1,15 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Backup\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; -class Create extends \Magento\Backup\Controller\Adminhtml\Index +/** + * Create backup controller + */ +class Create extends \Magento\Backup\Controller\Adminhtml\Index implements HttpPostActionInterface { /** * Create backup action. @@ -55,7 +58,9 @@ public function execute() $this->_coreRegistry->register('backup_manager', $backupManager); if ($this->getRequest()->getParam('maintenance_mode')) { - if (!$this->maintenanceMode->set(true)) { + $this->maintenanceMode->set(true); + + if (!$this->maintenanceMode->isOn()) { $response->setError( __( 'You need more permissions to activate maintenance mode right now.' diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php index 0451f6ed09bd1..7f450e7e313cc 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php @@ -6,13 +6,16 @@ */ namespace Magento\Backup\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; /** + * Backup rollback controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Rollback extends \Magento\Backup\Controller\Adminhtml\Index +class Rollback extends \Magento\Backup\Controller\Adminhtml\Index implements HttpPostActionInterface { /** * Rollback Action @@ -82,7 +85,9 @@ public function execute() } if ($this->getRequest()->getParam('maintenance_mode')) { - if (!$this->maintenanceMode->set(true)) { + $this->maintenanceMode->set(true); + + if (!$this->maintenanceMode->isOn()) { $response->setError( __( 'You need more permissions to activate maintenance mode right now.' @@ -122,6 +127,7 @@ public function execute() $adminSession->destroy(); $response->setRedirectUrl($this->getUrl('*')); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Backup\Exception\CantLoadSnapshot $e) { $errorMsg = __('We can\'t find the backup file.'); } catch (\Magento\Framework\Backup\Exception\FtpConnectionFailed $e) { diff --git a/app/code/Magento/Backup/Model/Backup.php b/app/code/Magento/Backup/Model/Backup.php index 3768f2bf8c8ce..c3507ecf5b459 100644 --- a/app/code/Magento/Backup/Model/Backup.php +++ b/app/code/Magento/Backup/Model/Backup.php @@ -14,6 +14,7 @@ * @method string getPath() * @method string getName() * @method string getTime() + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -80,6 +81,7 @@ class Backup extends \Magento\Framework\DataObject implements \Magento\Framework * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor * @param \Magento\Framework\Filesystem $filesystem * @param array $data + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Backup\Helper\Data $helper, @@ -242,7 +244,7 @@ public function setFile(&$content) /** * Return content of backup file * - * @return string + * @return array * @throws \Magento\Framework\Exception\LocalizedException */ public function &getFile() @@ -275,8 +277,9 @@ public function deleteFile() * * @param bool $write * @return $this - * @throws \Magento\Framework\Exception\InputException * @throws \Magento\Framework\Backup\Exception\NotEnoughPermissions + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\InputException */ public function open($write = false) { @@ -330,6 +333,7 @@ protected function _getStream() * * @param int $length * @return string + * @throws \Magento\Framework\Exception\InputException */ public function read($length) { @@ -340,6 +344,7 @@ public function read($length) * Check end of file. * * @return bool + * @throws \Magento\Framework\Exception\InputException */ public function eof() { @@ -370,6 +375,7 @@ public function write($string) * Close open backup file * * @return $this + * @throws \Magento\Framework\Exception\InputException */ public function close() { @@ -383,6 +389,8 @@ public function close() * Print output * * @return string + * @return \Magento\Framework\Filesystem\Directory\ReadInterface|string|void + * @throws \Magento\Framework\Exception\FileSystemException */ public function output() { @@ -398,6 +406,8 @@ public function output() } /** + * Get Size + * * @return int|mixed */ public function getSize() @@ -419,6 +429,7 @@ public function getSize() * * @param string $password * @return bool + * @throws \Exception */ public function validateUserPassword($password) { diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php index bc458a0a8e4bf..084b35448a823 100644 --- a/app/code/Magento/Backup/Model/Db.php +++ b/app/code/Magento/Backup/Model/Db.php @@ -6,6 +6,8 @@ namespace Magento\Backup\Model; use Magento\Backup\Helper\Data as Helper; +use Magento\Backup\Model\ResourceModel\Table\GetListTables; +use Magento\Backup\Model\ResourceModel\View\CreateViewsBackup; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\RuntimeException; @@ -44,18 +46,35 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface private $helper; /** - * @param \Magento\Backup\Model\ResourceModel\Db $resourceDb + * @var GetListTables + */ + private $getListTables; + + /** + * @var CreateViewsBackup + */ + private $getViewsBackup; + + /** + * Db constructor. + * @param ResourceModel\Db $resourceDb * @param \Magento\Framework\App\ResourceConnection $resource * @param Helper|null $helper + * @param GetListTables|null $getListTables + * @param CreateViewsBackup|null $getViewsBackup */ public function __construct( - \Magento\Backup\Model\ResourceModel\Db $resourceDb, + ResourceModel\Db $resourceDb, \Magento\Framework\App\ResourceConnection $resource, - ?Helper $helper = null + ?Helper $helper = null, + ?GetListTables $getListTables = null, + ?CreateViewsBackup $getViewsBackup = null ) { $this->_resourceDb = $resourceDb; $this->_resource = $resource; $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); + $this->getListTables = $getListTables ?? ObjectManager::getInstance()->get(GetListTables::class); + $this->getViewsBackup = $getViewsBackup ?? ObjectManager::getInstance()->get(CreateViewsBackup::class); } /** @@ -161,7 +180,7 @@ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backu $this->getResource()->beginTransaction(); - $tables = $this->getResource()->getTables(); + $tables = $this->getListTables->execute(); $backup->write($this->getResource()->getHeader()); @@ -198,6 +217,8 @@ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backu $backup->write($this->getResource()->getTableDataAfterSql($table)); } } + $this->getViewsBackup->execute($backup); + $backup->write($this->getResource()->getTableForeignKeysSql()); $backup->write($this->getResource()->getTableTriggersSql()); $backup->write($this->getResource()->getFooter()); diff --git a/app/code/Magento/Backup/Model/ResourceModel/Table/GetListTables.php b/app/code/Magento/Backup/Model/ResourceModel/Table/GetListTables.php new file mode 100644 index 0000000000000..73c4221feba3f --- /dev/null +++ b/app/code/Magento/Backup/Model/ResourceModel/Table/GetListTables.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backup\Model\ResourceModel\Table; + +use Magento\Framework\App\ResourceConnection; + +/** + * Provides full list of tables in the database. This list excludes views, to allow different backup process. + */ +class GetListTables +{ + private const TABLE_TYPE = 'BASE TABLE'; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->resource = $resource; + } + + /** + * Get list of database tables excluding views. + * + * @return array + */ + public function execute(): array + { + return $this->resource->getConnection('backup')->fetchCol( + "SHOW FULL TABLES WHERE `Table_type` = ?", + self::TABLE_TYPE + ); + } +} diff --git a/app/code/Magento/Backup/Model/ResourceModel/View/CreateViewsBackup.php b/app/code/Magento/Backup/Model/ResourceModel/View/CreateViewsBackup.php new file mode 100644 index 0000000000000..51b49dcb9e48a --- /dev/null +++ b/app/code/Magento/Backup/Model/ResourceModel/View/CreateViewsBackup.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backup\Model\ResourceModel\View; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Backup\Db\BackupInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; + +/** + * Creates backup of Views in the database. + */ +class CreateViewsBackup +{ + /** + * @var GetListViews + */ + private $getListViews; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @param GetListViews $getListViews + * @param ResourceConnection $resourceConnection + */ + public function __construct( + GetListViews $getListViews, + ResourceConnection $resourceConnection + ) { + $this->getListViews = $getListViews; + $this->resourceConnection = $resourceConnection; + } + + /** + * Write backup data to backup file. + * + * @param BackupInterface $backup + */ + public function execute(BackupInterface $backup): void + { + $views = $this->getListViews->execute(); + + foreach ($views as $view) { + $backup->write($this->getViewHeader($view)); + $backup->write($this->getDropViewSql($view)); + $backup->write($this->getCreateView($view)); + } + } + + /** + * Retrieve Database connection for Backup. + * + * @return AdapterInterface + */ + private function getConnection(): AdapterInterface + { + if (!$this->connection) { + $this->connection = $this->resourceConnection->getConnection('backup'); + } + + return $this->connection; + } + + /** + * Get CREATE VIEW query for the specific view. + * + * @param string $viewName + * @return string + */ + private function getCreateView(string $viewName): string + { + $quotedViewName = $this->getConnection()->quoteIdentifier($viewName); + $query = 'SHOW CREATE VIEW ' . $quotedViewName; + $row = $this->getConnection()->fetchRow($query); + $regExp = '/\sDEFINER\=\`([^`]*)\`\@\`([^`]*)\`/'; + $sql = preg_replace($regExp, '', $row['Create View']); + + return $sql . ';' . "\n"; + } + + /** + * Prepare a header for View being dumped. + * + * @param string $viewName + * @return string + */ + public function getViewHeader(string $viewName): string + { + $quotedViewName = $this->getConnection()->quoteIdentifier($viewName); + return "\n--\n" . "-- Structure for view {$quotedViewName}\n" . "--\n\n"; + } + + /** + * Make sure that View being created is deleted if already exists. + * + * @param string $viewName + * @return string + */ + public function getDropViewSql(string $viewName): string + { + $quotedViewName = $this->getConnection()->quoteIdentifier($viewName); + return sprintf('DROP VIEW IF EXISTS %s;\n', $quotedViewName); + } +} diff --git a/app/code/Magento/Backup/Model/ResourceModel/View/GetListViews.php b/app/code/Magento/Backup/Model/ResourceModel/View/GetListViews.php new file mode 100644 index 0000000000000..c76ea2842180b --- /dev/null +++ b/app/code/Magento/Backup/Model/ResourceModel/View/GetListViews.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backup\Model\ResourceModel\View; + +use Magento\Framework\App\ResourceConnection; + +/** + * Get list of database views. + */ +class GetListViews +{ + private const TABLE_TYPE = 'VIEW'; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->resource = $resource; + } + + /** + * Get list of database views. + * + * @return array + */ + public function execute(): array + { + return $this->resource->getConnection('backup')->fetchCol( + "SHOW FULL TABLES WHERE `Table_type` = ?", + self::TABLE_TYPE + ); + } +} diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml index 4f34f24c3a806..ebc4ac1fb056a 100644 --- a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml @@ -17,9 +17,10 @@ <click selector="{{AdminGridTableSection.backupRowCheckbox(backup.name)}}" stepKey="selectBackupRow"/> <selectOption selector="{{AdminGridActionSection.actionSelect}}" userInput="Delete" stepKey="selectDeleteAction"/> <click selector="{{AdminGridActionSection.submitButton}}" stepKey="clickSubmit"/> + <waitForPageLoad stepKey="waitForConfirmWindowToAppear"/> <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to delete the selected backup(s)?" stepKey="seeConfirmationModal"/> + <waitForPageLoad stepKey="waitForSubmitAction"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkConfirmDelete"/> <dontSee selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="dontSeeBackupInGrid"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml index ae97351cafcaf..ad218cdd57500 100644 --- a/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml +++ b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml @@ -20,4 +20,8 @@ <data key="name" unique="suffix">databaseBackup</data> <data key="type">Database</data> </entity> -</entities> + <entity name="WebSetupWizardBackup" type="backup"> + <data key="name">WebSetupWizard</data> + <data key="type">Database</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml index 90f6fa861b40f..aa6635b4dde4a 100644 --- a/app/code/Magento/Backup/etc/adminhtml/system.xml +++ b/app/code/Magento/Backup/etc/adminhtml/system.xml @@ -26,6 +26,7 @@ <label>Scheduled Backup Type</label> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> <source_model>Magento\Backup\Model\Config\Source\Type</source_model> </field> @@ -33,12 +34,14 @@ <label>Start Time</label> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> </field> <field id="frequency" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Frequency</label> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> <source_model>Magento\Cron\Model\Config\Source\Frequency</source_model> <backend_model>Magento\Backup\Model\Config\Backend\Cron</backend_model> @@ -48,6 +51,7 @@ <comment>Please put your store into maintenance mode during backup.</comment> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php index 418cb93900610..ea8a44a1122b4 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php +++ b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php @@ -75,7 +75,7 @@ public function execute() $this->logger->critical($e); $this->messageManager->addExceptionMessage( $e, - 'The order #' . $quote->getReservedOrderId() . ' cannot be processed.' + __('The order #%1 cannot be processed.', $quote->getReservedOrderId()) ); } diff --git a/app/code/Magento/Braintree/Controller/Paypal/Review.php b/app/code/Magento/Braintree/Controller/Paypal/Review.php index 14ec829d98024..2923db6fa88c3 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/Review.php +++ b/app/code/Magento/Braintree/Controller/Paypal/Review.php @@ -13,17 +13,24 @@ use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Payment\Model\Method\Logger; /** * Class Review */ -class Review extends AbstractAction implements HttpPostActionInterface +class Review extends AbstractAction implements HttpPostActionInterface, HttpGetActionInterface { /** * @var QuoteUpdater */ private $quoteUpdater; + /** + * @var Logger + */ + private $logger; + /** * @var string */ @@ -36,15 +43,18 @@ class Review extends AbstractAction implements HttpPostActionInterface * @param Config $config * @param Session $checkoutSession * @param QuoteUpdater $quoteUpdater + * @param Logger $logger */ public function __construct( Context $context, Config $config, Session $checkoutSession, - QuoteUpdater $quoteUpdater + QuoteUpdater $quoteUpdater, + Logger $logger ) { parent::__construct($context, $config, $checkoutSession); $this->quoteUpdater = $quoteUpdater; + $this->logger = $logger; } /** @@ -56,6 +66,7 @@ public function execute() $this->getRequest()->getPostValue('result', '{}'), true ); + $this->logger->debug($requestData); $quote = $this->checkoutSession->getQuote(); try { diff --git a/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php b/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php new file mode 100644 index 0000000000000..a6c1b088400a7 --- /dev/null +++ b/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Model\Multishipping; + +use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Braintree\Model\Ui\ConfigProvider; +use Magento\Braintree\Observer\DataAssignObserver; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider as PaypalConfigProvider; +use Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderPaymentExtensionInterface; +use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory; +use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Api\OrderManagementInterface; +use Magento\Vault\Api\Data\PaymentTokenInterface; + +/** + * Order payments processing for multishipping checkout flow. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PlaceOrder implements PlaceOrderInterface +{ + /** + * @var OrderManagementInterface + */ + private $orderManagement; + + /** + * @var OrderPaymentExtensionInterfaceFactory + */ + private $paymentExtensionFactory; + + /** + * @var GetPaymentNonceCommand + */ + private $getPaymentNonceCommand; + + /** + * @param OrderManagementInterface $orderManagement + * @param OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory + * @param GetPaymentNonceCommand $getPaymentNonceCommand + */ + public function __construct( + OrderManagementInterface $orderManagement, + OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory, + GetPaymentNonceCommand $getPaymentNonceCommand + ) { + $this->orderManagement = $orderManagement; + $this->paymentExtensionFactory = $paymentExtensionFactory; + $this->getPaymentNonceCommand = $getPaymentNonceCommand; + } + + /** + * @inheritdoc + */ + public function place(array $orderList): array + { + if (empty($orderList)) { + return []; + } + + $errorList = []; + $firstOrder = $this->orderManagement->place(array_shift($orderList)); + // get payment token from first placed order + $paymentToken = $this->getPaymentToken($firstOrder); + + foreach ($orderList as $order) { + try { + /** @var OrderInterface $order */ + $orderPayment = $order->getPayment(); + $this->setVaultPayment($orderPayment, $paymentToken); + $this->orderManagement->place($order); + } catch (\Exception $e) { + $incrementId = $order->getIncrementId(); + $errorList[$incrementId] = $e; + } + } + + return $errorList; + } + + /** + * Sets vault payment method. + * + * @param OrderPaymentInterface $orderPayment + * @param PaymentTokenInterface $paymentToken + * @return void + */ + private function setVaultPayment(OrderPaymentInterface $orderPayment, PaymentTokenInterface $paymentToken): void + { + $vaultMethod = $this->getVaultPaymentMethod( + $orderPayment->getMethod() + ); + $orderPayment->setMethod($vaultMethod); + + $publicHash = $paymentToken->getPublicHash(); + $customerId = $paymentToken->getCustomerId(); + $result = $this->getPaymentNonceCommand->execute( + ['public_hash' => $publicHash, 'customer_id' => $customerId] + ) + ->get(); + + $orderPayment->setAdditionalInformation( + DataAssignObserver::PAYMENT_METHOD_NONCE, + $result['paymentMethodNonce'] + ); + $orderPayment->setAdditionalInformation( + PaymentTokenInterface::PUBLIC_HASH, + $publicHash + ); + $orderPayment->setAdditionalInformation( + PaymentTokenInterface::CUSTOMER_ID, + $customerId + ); + } + + /** + * Returns vault payment method. + * + * For placing sequence of orders, we need to replace the original method on the vault method. + * + * @param string $method + * @return string + */ + private function getVaultPaymentMethod(string $method): string + { + $vaultPaymentMap = [ + ConfigProvider::CODE => ConfigProvider::CC_VAULT_CODE, + PaypalConfigProvider::PAYPAL_CODE => PaypalConfigProvider::PAYPAL_VAULT_CODE + ]; + + return $vaultPaymentMap[$method] ?? $method; + } + + /** + * Returns payment token. + * + * @param OrderInterface $order + * @return PaymentTokenInterface + * @throws \BadMethodCallException + */ + private function getPaymentToken(OrderInterface $order): PaymentTokenInterface + { + $orderPayment = $order->getPayment(); + $extensionAttributes = $this->getExtensionAttributes($orderPayment); + $paymentToken = $extensionAttributes->getVaultPaymentToken(); + + if ($paymentToken === null) { + throw new \BadMethodCallException('Vault Payment Token should be defined for placed order payment.'); + } + + return $paymentToken; + } + + /** + * Gets payment extension attributes. + * + * @param OrderPaymentInterface $payment + * @return OrderPaymentExtensionInterface + */ + private function getExtensionAttributes(OrderPaymentInterface $payment): OrderPaymentExtensionInterface + { + $extensionAttributes = $payment->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->paymentExtensionFactory->create(); + $payment->setExtensionAttributes($extensionAttributes); + } + + return $extensionAttributes; + } +} diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php index aa23fa767d1ed..ae2b1b1423640 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php @@ -123,8 +123,8 @@ private function updateShippingAddress(Quote $quote, array $details) { $shippingAddress = $quote->getShippingAddress(); - $shippingAddress->setLastname($details['lastName']); - $shippingAddress->setFirstname($details['firstName']); + $shippingAddress->setLastname($this->getShippingRecipientLastName($details)); + $shippingAddress->setFirstname($this->getShippingRecipientFirstName($details)); $shippingAddress->setEmail($details['email']); $shippingAddress->setCollectShippingRates(true); @@ -188,4 +188,30 @@ private function updateAddressData(Address $address, array $addressData) $address->setSameAsBilling(false); $address->setCustomerAddressId(null); } + + /** + * Returns shipping recipient first name. + * + * @param array $details + * @return string + */ + private function getShippingRecipientFirstName(array $details) + { + return isset($details['shippingAddress']['recipientName']) + ? explode(' ', $details['shippingAddress']['recipientName'], 2)[0] + : $details['firstName']; + } + + /** + * Returns shipping recipient last name. + * + * @param array $details + * @return string + */ + private function getShippingRecipientLastName(array $details) + { + return isset($details['shippingAddress']['recipientName']) + ? explode(' ', $details['shippingAddress']['recipientName'], 2)[1] + : $details['lastName']; + } } diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml index 412513c59c63c..ce1d0a9aecc90 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminOrderBraintreeFillActionGroup"> <!--Select Braintree Payment method on Admin Order Create Page--> <click stepKey="chooseBraintree" selector="{{NewOrderSection.creditCardBraintree}}"/> @@ -19,24 +19,24 @@ <!--Choose Master Card from drop-down list--> <switchToIFrame stepKey="switchToCardNumber" selector="{{NewOrderSection.cardFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.creditCardNumber}}" stepKey="waitForFillCardNumber"/> <fillField stepKey="fillCardNumber" selector="{{NewOrderSection.creditCardNumber}}" userInput="{{PaymentAndShippingInfo.cardNumber}}"/> - <waitForPageLoad stepKey="waitForFillCardNumber"/> <switchToIFrame stepKey="switchBackFromCard"/> <!--Fill expire date--> <switchToIFrame stepKey="switchToExpirationMonth" selector="{{NewOrderSection.monthFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.expirationMonth}}" stepKey="waitForFillMonth"/> <fillField stepKey="fillMonth" selector="{{NewOrderSection.expirationMonth}}" userInput="{{PaymentAndShippingInfo.month}}"/> - <waitForPageLoad stepKey="waitForFillMonth"/> <switchToIFrame stepKey="switchBackFromMonth"/> <switchToIFrame stepKey="switchToExpirationYear" selector="{{NewOrderSection.yearFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.expirationYear}}" stepKey="waitForFillYear"/> <fillField stepKey="fillYear" selector="{{NewOrderSection.expirationYear}}" userInput="{{PaymentAndShippingInfo.year}}"/> - <waitForPageLoad stepKey="waitForFillYear"/> <switchToIFrame stepKey="switchBackFromYear"/> <!--Fill CVW code--> <switchToIFrame stepKey="switchToCVV" selector="{{NewOrderSection.cvvFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.cvv}}" stepKey="waitForFillCVV"/> <fillField stepKey="fillCVV" selector="{{NewOrderSection.cvv}}" userInput="{{PaymentAndShippingInfo.cvv}}"/> - <wait stepKey="waitForFillCVV" time="1"/> <switchToIFrame stepKey="switchBackFromCVV"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml similarity index 83% rename from app/code/Magento/User/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml rename to app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml index d8a6a60299f8e..09ac0b77f861d 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="GoToUserRoles"> <click selector="#menu-magento-backend-system" stepKey="clickOnSystemIcon"/> <waitForPageLoad stepKey="waitForSystemsPageToOpen"/> @@ -15,24 +16,24 @@ </actionGroup> <!--Create new role--> - <actionGroup name="AdminCreateRole"> + <actionGroup name="AdminCreateNewRole"> <arguments> <argument name="role" type="string" defaultValue=""/> <argument name="resource" type="string" defaultValue="All"/> + <argument name="scope" type="string" defaultValue="Custom"/> + <argument name="websites" type="string" defaultValue="Main Website"/> </arguments> <click selector="{{AdminCreateRoleSection.create}}" stepKey="clickToAddNewRole"/> <fillField selector="{{AdminCreateRoleSection.name}}" userInput="{{role.name}}" stepKey="setRoleName"/> <fillField stepKey="setPassword" selector="{{AdminCreateRoleSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> <waitForPageLoad stepKey="waitForRoleResourcePage" time="5"/> - <click selector="{{AdminCreateRoleSection.roleScope}}" stepKey="clickToExpandScopeAccess"/> - <click selector="{{AdminCreateRoleSection.scopeValue(resource)}}" stepKey="clickToSelectScopeAccess"/> + <click stepKey="checkSales" selector="//a[text()='Sales']"/> <click selector="{{AdminCreateRoleSection.save}}" stepKey="clickToSaveRole"/> <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see userInput="You saved the role." stepKey="seeSuccessMessage" /> </actionGroup> - <!--Delete role--> <actionGroup name="AdminDeleteRoleActionGroup"> <arguments> @@ -46,4 +47,4 @@ <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see stepKey="seeSuccessMessage" userInput="You deleted the role."/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml similarity index 91% rename from app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml rename to app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml index 3e776df9fb97f..3f8bdaa4cd6bd 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Go to all users--> <actionGroup name="GoToAllUsers"> <click selector="{{AdminCreateUserSection.system}}" stepKey="clickOnSystemIcon"/> @@ -39,11 +39,10 @@ <see userInput="You saved the user." stepKey="seeSuccessMessage" /> </actionGroup> - <!--Delete User--> - <actionGroup name="AdminDeleteUserActionGroup"> + <actionGroup name="AdminDeleteNewUserActionGroup"> + <click stepKey="clickOnUser" selector="{{AdminDeleteUserSection.theUser}}"/> - <waitForPageLoad stepKey="waitForUserPageToLoad"/> <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> @@ -52,4 +51,5 @@ <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see userInput="You deleted the user." stepKey="seeSuccessMessage" /> </actionGroup> + </actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml index 9eaae8b33e73f..cbb065704fbc1 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml @@ -12,7 +12,7 @@ <!-- GoTo ConfigureBraintree fields --> <click stepKey="clickOnSTORES" selector="{{AdminMenuSection.stores}}"/> <waitForPageLoad stepKey="waitForConfiguration" time="2"/> - <click stepKey="clickOnConfigurations" selector="{{StoresSubmenuSection.configuration}}" /> + <click stepKey="clickOnConfigurations" selector="{{AdminMenuSection.configuration}}" /> <waitForPageLoad stepKey="waitForSales" time="2"/> <click stepKey="clickOnSales" selector="{{ConfigurationListSection.sales}}" /> <waitForPageLoad stepKey="waitForPaymentMethods" time="2"/> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml deleted file mode 100644 index 17d634c009b3e..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - - <actionGroup name="useBraintreeForMasterCard"> - <click stepKey="chooseBraintree" selector="{{NewOrderSection.creditCardBraintree}}"/> - <waitForPageLoad stepKey="waitForBraintreeConfigs" time="5"/> - <click stepKey="openCardTypes" selector="{{NewOrderSection.openCardTypes}}"/> - <waitForPageLoad stepKey="waitForCardTypes" time="3"/> - <click stepKey="chooseCardType" selector="{{NewOrderSection.masterCard}}"/> - <waitForPageLoad stepKey="waitForCardSelected" time="3"/> - - <switchToIFrame stepKey="switchToCardNumber" selector="{{NewOrderSection.cardFrame}}"/> - <fillField stepKey="fillCardNumber" selector="{{NewOrderSection.creditCardNumber}}" userInput="{{PaymentAndShippingInfo.cardNumber}}"/> - <waitForPageLoad stepKey="waitForFillCardNumber" time="1"/> - <switchToIFrame stepKey="switchBackFromCard"/> - - <switchToIFrame stepKey="switchToExpirationMonth" selector="{{NewOrderSection.monthFrame}}"/> - <fillField stepKey="fillMonth" selector="{{NewOrderSection.expirationMonth}}" userInput="{{PaymentAndShippingInfo.month}}"/> - <waitForPageLoad stepKey="waitForFillMonth" time="1"/> - <switchToIFrame stepKey="switchBackFromMonth"/> - - <switchToIFrame stepKey="switchToExpirationYear" selector="{{NewOrderSection.yearFrame}}"/> - <fillField stepKey="fillYear" selector="{{NewOrderSection.expirationYear}}" userInput="{{PaymentAndShippingInfo.year}}"/> - <waitForPageLoad stepKey="waitForFillYear" time="1"/> - <switchToIFrame stepKey="switchBackFromYear"/> - - <switchToIFrame stepKey="switchToCVV" selector="{{NewOrderSection.cvvFrame}}"/> - <fillField stepKey="fillCVV" selector="{{NewOrderSection.cvv}}" userInput="{{PaymentAndShippingInfo.cvv}}"/> - <wait stepKey="waitForFillCVV" time="1"/> - <switchToIFrame stepKey="switchBackFromCVV"/> - </actionGroup> -</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml deleted file mode 100644 index 4d531214db150..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?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="DeleteCustomerActionGroup"> - <arguments> - <argument name="lastName" defaultValue=""/> - </arguments> - <!--Clear filter if exist--> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingCustomerFilters"/> - - <click stepKey="chooseCustomer" selector="{{CustomersPageSection.customerCheckbox(lastName)}}"/> - <waitForAjaxLoad stepKey="waitForThick" time="2"/> - <click stepKey="OpenActions" selector="{{CustomersPageSection.actions}}"/> - <waitForAjaxLoad stepKey="waitForDelete" time="5"/> - <click stepKey="ChooseDelete" selector="{{CustomersPageSection.delete}}"/> - <waitForPageLoad stepKey="waitForDeleteItemPopup" time="10"/> - <click stepKey="clickOnOk" selector="{{CustomersPageSection.ok}}"/> - <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="10"/> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml index bc6d6c2b46dc9..bf06bc7df5201 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml @@ -6,24 +6,27 @@ */ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontFillCartDataActionGroup"> <arguments> <argument name="cartData" defaultValue="PaymentAndShippingInfo"/> </arguments> <switchToIFrame selector="{{BraintreeConfigurationPaymentSection.cartFrame}}" stepKey="switchToIframe"/> + <waitForElementVisible selector="{{BraintreeConfigurationPaymentSection.cartCode}}" stepKey="waitCartCodeElement"/> <fillField selector="{{BraintreeConfigurationPaymentSection.cartCode}}" userInput="{{cartData.cardNumber}}" stepKey="setCartCode"/> <switchToIFrame stepKey="switchBack"/> <switchToIFrame selector="{{BraintreeConfigurationPaymentSection.monthFrame}}" stepKey="switchToIframe1"/> + <waitForElementVisible selector="{{BraintreeConfigurationPaymentSection.month}}" stepKey="waitMonthElement"/> <fillField selector="{{BraintreeConfigurationPaymentSection.month}}" userInput="{{cartData.month}}" stepKey="setMonth"/> <switchToIFrame stepKey="switchBack1"/> <switchToIFrame selector="{{BraintreeConfigurationPaymentSection.yearFrame}}" stepKey="switchToIframe2"/> + <waitForElementVisible selector="{{BraintreeConfigurationPaymentSection.year}}" stepKey="waitYearElement"/> <fillField selector="{{BraintreeConfigurationPaymentSection.year}}" userInput="{{cartData.year}}" stepKey="setYear"/> <switchToIFrame stepKey="switchBack2"/> <switchToIFrame selector="{{BraintreeConfigurationPaymentSection.codeFrame}}" stepKey="switchToIframe3"/> + <waitForElementVisible selector="{{BraintreeConfigurationPaymentSection.verificationNumber}}" stepKey="waitVerificationNumber"/> <fillField selector="{{BraintreeConfigurationPaymentSection.verificationNumber}}" userInput="{{cartData.cvv}}" stepKey="setVerificationNumber"/> <switchToIFrame stepKey="SwitchBackToWindow"/> - </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml deleted file mode 100644 index 30345ec31bacd..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="NewCustomerData" type="braintree_config_state"> - <data key="FirstName">Abgar</data> - <data key="LastName">Abgaryan</data> - <data key="Email">m@m.com</data> - <data key="AddressFirstName">Abgar</data> - <data key="AddressLastName">Abgaryan</data> - <data key="StreetAddress">Street</data> - <data key="City">Yerevan</data> - <data key="Zip">9999</data> - <data key="PhoneNumber">9999</data> - <data key="Country">Armenia</data> - </entity> - -</entities> diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml deleted file mode 100644 index 72661ae94076f..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="NewProductData" type="braintree_config_state"> - <data key="ProductName">ProductTest</data> - <data key="Price">100</data> - <data key="Quantity">100</data> - </entity> - -</entities> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml deleted file mode 100644 index 220c9a444b02f..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="AdminDeleteRoleSection"> - <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> - <element name="current_pass" type="button" selector="#current_password"/> - <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> - <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> - </section> -</sections> \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml deleted file mode 100644 index bf2e2b44eb602..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="AdminDeleteUserSection"> - <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> - <element name="password" selector="#user_current_password" type="input"/> - <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> - <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> - </section> -</sections> \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml index e37ce8f4738b3..a34cdf15e7ad7 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEditRoleInfoSection"> <element name="roleName" type="input" selector="#role_name"/> <element name="password" type="input" selector="#current_password"/> @@ -18,4 +20,4 @@ <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml index e999413c96d74..216292b81162c 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEditUserRoleSection"> <element name="usernameTextField" type="input" selector="#user_username"/> <element name="roleNameFilterTextField" type="input" selector="#permissionsUserRolesGrid_filter_role_name"/> @@ -14,4 +16,4 @@ <element name="roleNameInFirstRow" type="text" selector=".col-role_name"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml index 2e5fcfb7b5c8d..cee262864d8ca 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEditUserSection"> <element name="system" type="input" selector="#menu-magento-backend-system"/> <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> @@ -25,4 +27,4 @@ <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> <element name="saveButton" type="button" selector="#save"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml index eb7a9ce2c376e..24e5efdc610ff 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminMenuSection"> <element name="dashboard" type="button" selector="//li[@id='menu-magento-backend-dashboard']"/> <element name="sales" type="button" selector="//li[@id='menu-magento-sales-sales']"/> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml index 63cbadc71d3d3..1cf54bf94e772 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminRoleGridSection"> <element name="idFilterTextField" type="input" selector="#roleGrid_filter_role_id"/> <element name="roleNameFilterTextField" type="input" selector="#roleGrid_filter_role_name"/> @@ -14,4 +16,4 @@ <element name="roleNameInFirstRow" type="text" selector=".col-role_name"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml deleted file mode 100644 index 9564bc61f799c..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="AdminUserGridSection"> - <element name="usernameFilterTextField" type="input" selector="#permissionsUserGrid_filter_username"/> - <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> - <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> - <element name="usernameInFirstRow" type="text" selector=".col-username"/> - <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> - <element name="successMessage" type="text" selector=".message-success"/> - </section> -</sections> \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml index 016af2e102744..f8802e9a34ae5 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BraintreeConfiguraionSection"> <element name="titleForBraintreeSettings" type="input" selector="//input[@id='payment_us_braintree_section_braintree_braintree_required_title']"/> <element name="environment" type="select" selector="//select[@id='payment_us_braintree_section_braintree_braintree_required_environment']"/> @@ -29,6 +31,5 @@ <element name="actionAuthorize" type="text" selector="//select[@id='payment_us_braintree_section_braintree_braintree_paypal_payment_action']/option[text()='Authorize']"/> <element name="save" type="button" selector="//span[text()='Save Config']"/> <element name="successfulMessage" type="text" selector="//*[@data-ui-id='messages-message-success']"/> - </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml deleted file mode 100644 index 32f02a69f817e..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="CatalogSubmenuSection"> - <element name="products" type="button" selector="//li[@id='menu-magento-catalog-catalog']//li[@data-ui-id='menu-magento-catalog-catalog-products']"/> - </section> -</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml deleted file mode 100644 index 100407438eaae..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="ConfigurationListSection"> - <element name="sales" type="button" selector="//div[contains(@class, 'admin__page-nav-title title _collapsible')]/strong[text()='Sales']"/> - <element name="salesPaymentMethods" type="button" selector="//span[text()='Payment Methods']"/> - </section> -</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml index 885a45be721f1..2192dd935c331 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ConfigurationPaymentSection"> <element name="configureButton" type="button" selector="//button[@id='payment_us_braintree_section_braintree-head']"/> </section> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml deleted file mode 100644 index e4a75b1b6a842..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="CustomersPageSection"> - <element name="addNewCustomerButton" type="button" selector="//*[@id='add']"/> - <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> - <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']"/> - <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> - <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> - <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> - </section> -</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml deleted file mode 100644 index 937afb83da96f..0000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="CustomersSubmenuSection"> - <element name="allCustomers" type="button" selector="//li[@id='menu-magento-customer-customer']//li[@data-ui-id='menu-magento-customer-customer-manage']"/> - </section> -</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml index f094baa9f3446..806762f826462 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StoresSubmenuSection"> <element name="configuration" type="button" selector="//li[@id='menu-magento-backend-stores']//li[@data-ui-id='menu-magento-config-system-config']"/> </section> diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml b/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml index f27477ce8a672..a781841e0a77b 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml @@ -81,16 +81,12 @@ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="LoggedInCheckoutFillNewBillingAddressActionGroup1"> <argument name="Address" value="US_Address_NY"/> </actionGroup> - <click selector="{{CheckoutPaymentSection.addressAction('Save Address')}}" stepKey="SaveAddress"/> + <click selector="{{CheckoutPaymentSection.addressAction('Ship here')}}" stepKey="SaveAddress"/> <waitForPageLoad stepKey="waitForPageLoad9"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> <waitForPageLoad stepKey="waitForPageLoad10"/> <click selector="{{BraintreeConfigurationPaymentSection.paymentMethod}}" stepKey="SelectBraintreePaymentMethod1"/> <waitForPageLoad stepKey="waitForPageLoad11"/> - <click selector="{{CheckoutPaymentSection.shippingAndBillingAddressSame}}" stepKey="UncheckCheckBox"/> - - <click selector="{{CheckoutShippingSection.updateAddress}}" stepKey="clickToUpdate"/> - <waitForPageLoad stepKey="waitForPageLoad12"/> <!--Place order--> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="PlaceOrder1"/> <waitForPageLoad stepKey="waitForPageLoad13"/> diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml index 2ddefa40b536c..244052371e702 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml @@ -40,16 +40,18 @@ <!--Create New Role--> <actionGroup ref="GoToUserRoles" stepKey="GoToUserRoles"/> - <actionGroup ref="AdminCreateRole" stepKey="AdminCreateNewRole"/> + <waitForPageLoad stepKey="waitForAllRoles" time="15"/> + <actionGroup ref="AdminCreateNewRole" stepKey="AdminCreateNewRole"/> - <!--Create New User With Specific Role--> + <!--Create new admin user--> <actionGroup ref="GoToAllUsers" stepKey="GoToAllUsers"/> + <waitForPageLoad stepKey="waitForUsers" time="15"/> <actionGroup ref="AdminCreateUserAction" stepKey="AdminCreateNewUser"/> <!--SignOut--> <actionGroup ref="logout" stepKey="signOutFromAdmin"/> - <!--SignIn New User--> + <!--Log in as new user--> <actionGroup ref="LoginNewUser" stepKey="signInNewUser"/> <waitForPageLoad stepKey="waitForLogin" time="3"/> @@ -58,24 +60,28 @@ <argument name="customer" value="Simple_US_Customer"/> </actionGroup> + <!--Add Product to Order--> <actionGroup ref="addSimpleProductToOrder" stepKey="addProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> + <!--Fill Order Customer Information--> <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> <argument name="customer" value="Simple_US_Customer"/> <argument name="address" value="US_Address_TX"/> </actionGroup> + <!--Select Shipping--> <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> - <waitForPageLoad stepKey="waitForShippingToFinish"/> + <!--Pay with Braintree --> <actionGroup ref="useBraintreeForMasterCard" stepKey="selectCardWithBraintree"/> + <!--Submit Order--> <click stepKey="submitOrder" selector="{{NewOrderSection.submitOrder}}"/> - <waitForPageLoad stepKey="waitForSaveConfig" time="5"/> - <waitForElementVisible selector="{{NewOrderSection.successMessage}}" stepKey="waitForSuccessMessage" time="1"/> + <waitForPageLoad stepKey="waitForSaveConfig"/> + <waitForElementVisible selector="{{NewOrderSection.successMessage}}" stepKey="waitForSuccessMessage"/> <after> <!-- Disable BrainTree --> @@ -93,7 +99,7 @@ <!--Delete User --> <actionGroup ref="GoToAllUsers" stepKey="GoBackToAllUsers"/> - <actionGroup ref="AdminDeleteUserActionGroup" stepKey="AdminDeleteUserActionGroup"/> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="AdminDeleteUserActionGroup"/> <!--Delete Role--> <actionGroup ref="GoToUserRoles" stepKey="GoBackToUserRoles"/> diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php index 609b7f21dbf87..d68838bafbf0e 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php @@ -6,6 +6,7 @@ namespace Magento\Braintree\Test\Unit\Controller\Paypal; +use Magento\Payment\Model\Method\Logger; use Magento\Quote\Model\Quote; use Magento\Framework\View\Layout; use Magento\Checkout\Model\Session; @@ -65,6 +66,11 @@ class ReviewTest extends \PHPUnit\Framework\TestCase */ private $review; + /** + * @var Logger|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + protected function setUp() { /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ @@ -88,6 +94,9 @@ protected function setUp() ->getMock(); $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); $contextMock->expects(self::once()) ->method('getRequest') @@ -103,7 +112,8 @@ protected function setUp() $contextMock, $this->configMock, $this->checkoutSessionMock, - $this->quoteUpdaterMock + $this->quoteUpdaterMock, + $this->loggerMock ); } diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php index a2b5380d2884b..c2678d1c78437 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php @@ -165,7 +165,7 @@ private function getDetails(): array 'region' => 'IL', 'postalCode' => '60618', 'countryCodeAlpha2' => 'US', - 'recipientName' => 'John Doe', + 'recipientName' => 'Jane Smith', ], 'billingAddress' => [ 'streetAddress' => '123 Billing Street', @@ -186,9 +186,9 @@ private function getDetails(): array private function updateShippingAddressStep(array $details): void { $this->shippingAddress->method('setLastname') - ->with($details['lastName']); + ->with('Smith'); $this->shippingAddress->method('setFirstname') - ->with($details['firstName']); + ->with('Jane'); $this->shippingAddress->method('setEmail') ->with($details['email']); $this->shippingAddress->method('setCollectShippingRates') diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php b/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php index 372415d3530c0..55e76cae9103a 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php @@ -40,7 +40,7 @@ public function __get($name) } /** - * Checks for the existance of a property stored in the private $_attributes property + * Checks for the existence of a property stored in the private $_attributes property * * @ignore * @param string $name diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 5af56a2afd3fe..2f956076f3846 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -21,7 +21,8 @@ "magento/module-quote": "*", "magento/module-sales": "*", "magento/module-ui": "*", - "magento/module-vault": "*" + "magento/module-vault": "*", + "magento/module-multishipping": "*" }, "suggest": { "magento/module-checkout-agreements": "*", diff --git a/app/code/Magento/Braintree/etc/config.xml b/app/code/Magento/Braintree/etc/config.xml index 9de4773af023a..fe4cfab9c0e30 100644 --- a/app/code/Magento/Braintree/etc/config.xml +++ b/app/code/Magento/Braintree/etc/config.xml @@ -42,7 +42,7 @@ <paymentInfoKeys>cc_type,cc_number,avsPostalCodeResponseCode,avsStreetAddressResponseCode,cvvResponseCode,processorAuthorizationCode,processorResponseCode,processorResponseText,liabilityShifted,liabilityShiftPossible,riskDataId,riskDataDecision</paymentInfoKeys> <avs_ems_adapter>Magento\Braintree\Model\AvsEmsCodeMapper</avs_ems_adapter> <cvv_ems_adapter>Magento\Braintree\Model\CvvEmsCodeMapper</cvv_ems_adapter> - <group>braintree</group> + <group>braintree_group</group> </braintree> <braintree_paypal> <model>BraintreePayPalFacade</model> @@ -68,7 +68,7 @@ <privateInfoKeys>processorResponseCode,processorResponseText,paymentId</privateInfoKeys> <paymentInfoKeys>processorResponseCode,processorResponseText,paymentId,payerEmail</paymentInfoKeys> <supported_locales>en_US,en_GB,en_AU,da_DK,fr_FR,fr_CA,de_DE,zh_HK,it_IT,nl_NL,no_NO,pl_PL,es_ES,sv_SE,tr_TR,pt_BR,ja_JP,id_ID,ko_KR,pt_PT,ru_RU,th_TH,zh_CN,zh_TW</supported_locales> - <group>braintree</group> + <group>braintree_group</group> </braintree_paypal> <braintree_cc_vault> <model>BraintreeCreditCardVaultFacade</model> @@ -78,7 +78,7 @@ <tokenFormat>Magento\Braintree\Model\InstantPurchase\CreditCard\TokenFormatter</tokenFormat> <additionalInformation>Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider</additionalInformation> </instant_purchase> - <group>braintree</group> + <group>braintree_group</group> </braintree_cc_vault> <braintree_paypal_vault> <model>BraintreePayPalVaultFacade</model> @@ -88,7 +88,7 @@ <tokenFormat>Magento\Braintree\Model\InstantPurchase\PayPal\TokenFormatter</tokenFormat> <additionalInformation>Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider</additionalInformation> </instant_purchase> - <group>braintree</group> + <group>braintree_group</group> </braintree_paypal_vault> </payment> </default> diff --git a/app/code/Magento/Braintree/etc/frontend/di.xml b/app/code/Magento/Braintree/etc/frontend/di.xml index ea417c407dffd..d8d3a93b71dc3 100644 --- a/app/code/Magento/Braintree/etc/frontend/di.xml +++ b/app/code/Magento/Braintree/etc/frontend/di.xml @@ -61,4 +61,12 @@ <argument name="resolver" xsi:type="object">Magento\Braintree\Model\LocaleResolver</argument> </arguments> </type> + <type name="Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderPool"> + <arguments> + <argument name="services" xsi:type="array"> + <item name="braintree" xsi:type="string">Magento\Braintree\Model\Multishipping\PlaceOrder</item> + <item name="braintree_paypal" xsi:type="string">Magento\Braintree\Model\Multishipping\PlaceOrder</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Braintree/etc/payment.xml b/app/code/Magento/Braintree/etc/payment.xml index dbabd91151022..4cae049aaf5a9 100644 --- a/app/code/Magento/Braintree/etc/payment.xml +++ b/app/code/Magento/Braintree/etc/payment.xml @@ -8,8 +8,16 @@ <payment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd"> <groups> - <group id="braintree"> + <group id="braintree_group"> <label>Braintree</label> </group> </groups> + <methods> + <method name="braintree"> + <allow_multiple_address>1</allow_multiple_address> + </method> + <method name="braintree_paypal"> + <allow_multiple_address>1</allow_multiple_address> + </method> + </methods> </payment> diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml index 535a5a852fe70..4c15fffa8189f 100644 --- a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml @@ -83,7 +83,7 @@ $ccType = $block->getInfoData('cc_type'); id="<?= /* @noEscape */ $code ?>_vault" name="payment[is_active_payment_token_enabler]" class="admin__control-checkbox"/> - <label class="label" for="<?= /* @noEscape */ $code ?>_vault"> + <label class="label admin__field-label" for="<?= /* @noEscape */ $code ?>_vault"> <span><?= $block->escapeHtml(__('Save for later use.')) ?></span> </label> </div> diff --git a/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 0000000000000..06390d403e63d --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout_billing"> + <arguments> + <argument name="form_templates" xsi:type="array"> + <item name="braintree" xsi:type="string">Magento_Braintree::multishipping/form.phtml</item> + <item name="braintree_paypal" xsi:type="string">Magento_Braintree::multishipping/form_paypal.phtml</item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml new file mode 100644 index 0000000000000..bf8aa8dd09c2c --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'braintree' + }; + layout([ + { + component: 'Magento_Braintree/js/view/payment/method-renderer/multishipping/hosted-fields', + name: 'payment_method_braintree', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> +<!-- ko template: getTemplate() --><!-- /ko --> diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml new file mode 100644 index 0000000000000..ea3eb2214c2d8 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'braintree_paypal' + }; + layout([ + { + component: 'Magento_Braintree/js/view/payment/method-renderer/multishipping/paypal', + name: 'payment_method_braintree_paypal', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> +<!-- ko template: getTemplate() --><!-- /ko --> diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js index 05c09abdb7b2e..9e496e43b27c5 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js @@ -10,8 +10,9 @@ define([ 'Magento_Braintree/js/view/payment/method-renderer/cc-form', 'Magento_Braintree/js/validator', 'Magento_Vault/js/view/payment/vault-enabler', - 'mage/translate' -], function ($, Component, validator, VaultEnabler, $t) { + 'mage/translate', + 'Magento_Checkout/js/model/payment/additional-validators' +], function ($, Component, validator, VaultEnabler, $t, additionalValidators) { 'use strict'; return Component.extend({ @@ -154,7 +155,7 @@ define([ * Trigger order placing */ placeOrderClick: function () { - if (this.validateCardType()) { + if (this.validateCardType() && additionalValidators.validate()) { this.isPlaceOrderActionAllowed(false); $(this.getSelector('submit')).trigger('click'); } diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js new file mode 100644 index 0000000000000..1ceebc8e66282 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js @@ -0,0 +1,102 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*browser:true*/ +/*global define*/ + +define([ + 'jquery', + 'Magento_Braintree/js/view/payment/method-renderer/hosted-fields', + 'Magento_Braintree/js/validator', + 'Magento_Ui/js/model/messageList', + 'mage/translate', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/model/payment/additional-validators' +], function ( + $, + Component, + validator, + messageList, + $t, + fullScreenLoader, + setPaymentInformationAction, + additionalValidators +) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Braintree/payment/multishipping/form' + }, + + /** + * Get list of available CC types + * + * @returns {Object} + */ + getCcAvailableTypes: function () { + var availableTypes = validator.getAvailableCardTypes(), + billingCountryId; + + billingCountryId = $('#multishipping_billing_country_id').val(); + + if (billingCountryId && validator.getCountrySpecificCardTypes(billingCountryId)) { + return validator.collectTypes( + availableTypes, validator.getCountrySpecificCardTypes(billingCountryId) + ); + } + + return availableTypes; + }, + + /** + * @override + */ + placeOrder: function () { + var self = this; + + this.validatorManager.validate(self, function () { + return self.setPaymentInformation(); + }); + }, + + /** + * @override + */ + setPaymentInformation: function () { + if (additionalValidators.validate()) { + + fullScreenLoader.startLoader(); + + $.when( + setPaymentInformationAction( + this.messageContainer, + this.getData() + ) + ).done(this.done.bind(this)) + .fail(this.fail.bind(this)); + } + }, + + /** + * {Function} + */ + fail: function () { + fullScreenLoader.stopLoader(); + + return this; + }, + + /** + * {Function} + */ + done: function () { + fullScreenLoader.stopLoader(); + $('#multishipping-billing-form').submit(); + + return this; + } + }); +}); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js new file mode 100644 index 0000000000000..6702e58d1214b --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js @@ -0,0 +1,143 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*browser:true*/ +/*global define*/ +define([ + 'jquery', + 'underscore', + 'Magento_Braintree/js/view/payment/method-renderer/paypal', + 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/model/payment/additional-validators', + 'Magento_Checkout/js/model/full-screen-loader', + 'mage/translate' +], function ( + $, + _, + Component, + setPaymentInformationAction, + additionalValidators, + fullScreenLoader, + $t +) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Braintree/payment/multishipping/paypal', + submitButtonSelector: '#payment-continue span' + }, + + /** + * @override + */ + onActiveChange: function (isActive) { + this.updateSubmitButtonTitle(isActive); + + this._super(isActive); + }, + + /** + * @override + */ + beforePlaceOrder: function (data) { + this._super(data); + + this.updateSubmitButtonTitle(true); + }, + + /** + * @override + */ + getShippingAddress: function () { + return {}; + }, + + /** + * @override + */ + getData: function () { + var data = this._super(); + + data['additional_data']['is_active_payment_token_enabler'] = true; + + return data; + }, + + /** + * @override + */ + isActiveVault: function () { + return true; + }, + + /** + * Skipping order review step on checkout with multiple addresses is not allowed. + * + * @returns {Boolean} + */ + isSkipOrderReview: function () { + return false; + }, + + /** + * Checks if payment method nonce is already received. + * + * @returns {Boolean} + */ + isPaymentMethodNonceReceived: function () { + return this.paymentMethodNonce !== null; + }, + + /** + * Updates submit button title on multi-addresses checkout billing form. + * + * @param {Boolean} isActive + */ + updateSubmitButtonTitle: function (isActive) { + var title = this.isPaymentMethodNonceReceived() || !isActive ? + $t('Go to Review Your Order') : $t('Continue to PayPal'); + + $(this.submitButtonSelector).html(title); + }, + + /** + * @override + */ + placeOrder: function () { + if (!this.isPaymentMethodNonceReceived()) { + this.payWithPayPal(); + } else { + fullScreenLoader.startLoader(); + + $.when( + setPaymentInformationAction( + this.messageContainer, + this.getData() + ) + ).done(this.done.bind(this)) + .fail(this.fail.bind(this)); + } + }, + + /** + * {Function} + */ + fail: function () { + fullScreenLoader.stopLoader(); + + return this; + }, + + /** + * {Function} + */ + done: function () { + fullScreenLoader.stopLoader(); + $('#multishipping-billing-form').submit(); + + return this; + } + }); +}); diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html new file mode 100644 index 0000000000000..964e15df166d3 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html @@ -0,0 +1,106 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div data-bind="attr: {class: 'payment-method payment-method-' + getCode()}, css: {'_active': isActive()}"> + <div> + <form id="co-transparent-form-braintree" class="form" data-bind="" method="post" action="#" novalidate="novalidate"> + <fieldset data-bind="attr: {class: 'fieldset payment items ccard ' + getCode(), id: 'payment_form_' + getCode()}"> + <legend class="legend"> + <span><!-- ko i18n: 'Credit Card Information'--><!-- /ko --></span> + </legend> + <br> + <div class="field type"> + <div class="control"> + <ul class="credit-card-types"> + <!-- ko foreach: {data: getCcAvailableTypes(), as: 'item'} --> + <li class="item" data-bind="css: { + _active: $parent.selectedCardType() == item, + _inactive: $parent.selectedCardType() != null && $parent.selectedCardType() != item + } "> + <!--ko if: $parent.getIcons(item) --> + <img data-bind="attr: { + 'src': $parent.getIcons(item).url, + 'width': $parent.getIcons(item).width, + 'height': $parent.getIcons(item).height + }"> + <!--/ko--> + </li> + <!--/ko--> + </ul> + <input type="hidden" + name="payment[cc_type]" + class="input-text" + value="" + data-bind="attr: {id: getCode() + '_cc_type', 'data-container': getCode() + '-cc-type'}, + value: creditCardType + "> + </div> + </div> + <div class="field number required"> + <label data-bind="attr: {for: getCode() + '_cc_number'}" class="label"> + <span><!-- ko i18n: 'Credit Card Number'--><!-- /ko --></span> + </label> + <div class="control"> + <div data-bind="attr: {id: getCode() + '_cc_number'}" class="hosted-control"></div> + <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Credit Card Number'--><!-- /ko --></div> + </div> + </div> + <div class="field number required"> + <label data-bind="attr: {for: getCode() + '_expiration'}" class="label"> + <span><!-- ko i18n: 'Expiration Date'--><!-- /ko --></span> + </label> + <div class="control"> + <div class="hosted-date-wrap"> + <div data-bind="attr: {id: getCode() + '_expirationMonth'}" + class="hosted-control hosted-date"></div> + + <div data-bind="attr: {id: getCode() + '_expirationYear'}" + class="hosted-control hosted-date"></div> + + <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Expiration Date'--><!-- /ko --></div> + </div> + </div> + </div> + <!-- ko if: (hasVerification())--> + <div class="field cvv required" data-bind="attr: {id: getCode() + '_cc_type_cvv_div'}"> + <label data-bind="attr: {for: getCode() + '_cc_cid'}" class="label"> + <span><!-- ko i18n: 'Card Verification Number'--><!-- /ko --></span> + </label> + <div class="control _with-tooltip"> + <div data-bind="attr: {id: getCode() + '_cc_cid'}" class="hosted-control hosted-cid"></div> + <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Card Verification Number'--><!-- /ko --></div> + + <div class="field-tooltip toggle"> + <span class="field-tooltip-action action-cvv" + tabindex="0" + data-toggle="dropdown" + data-bind="attr: {title: $t('What is this?')}, mageInit: {'dropdown':{'activeClass': '_active'}}"> + <span><!-- ko i18n: 'What is this?'--><!-- /ko --></span> + </span> + <div class="field-tooltip-content" + data-target="dropdown" + data-bind="html: getCvvImageHtml()"></div> + </div> + </div> + </div> + <!-- /ko --> + </fieldset> + <input type="submit" id="braintree_submit" style="display:none" /> + </form> + + <div class="actions-toolbar no-display"> + <div class="primary"> + <button data-role="review-save" + type="submit" + data-bind="{click: placeOrderClick}" + class="action primary checkout"> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html new file mode 100644 index 0000000000000..722989e41f98f --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html @@ -0,0 +1,40 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="payment-method" data-bind="css: {'_active': isActive()}"> + <div class="payment-method-title field choice"> + <label class="label" data-bind="attr: {'for': getCode()}"> + <!-- PayPal Logo --> + <img data-bind="attr: {src: getPaymentAcceptanceMarkSrc(), alt: $t('Acceptance Mark'), title: $t('Acceptance Mark')}" + class="payment-icon"/> + <!-- PayPal Logo --> + <span text="getTitle()"></span> + </label> + </div> + + <div class="payment-method-content"> + <each args="getRegion('messages')" render=""></each> + <fieldset class="braintree-paypal-fieldset" data-bind='attr: {id: "payment_form_" + getCode()}'> + <div id="paypal-container"></div> + </fieldset> + <div class="actions-toolbar braintree-paypal-actions" data-bind="visible: isReviewRequired()"> + <div class="payment-method-item braintree-paypal-account"> + <span class="payment-method-type">PayPal</span> + <span class="payment-method-description" text="customerEmail()"></span> + </div> + <div class="actions-toolbar no-display"> + <div class="primary"> + <button data-button="paypal-place" data-role="review-save" + type="submit" + data-bind="{click: placeOrder}" + class="action primary checkout"> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php index 0e21e566d5e75..a768e2450bfe8 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ +namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes; + /** - * Bundle Extended Attribures Block + * Bundle Extended Attribures Block. * - * @author Magento Core Team <core@magentocommerce.com> + * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes; - class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element { /** @@ -75,7 +75,7 @@ public function getElementHtml() } /** - * Execute method getElementHtml from parrent class + * Execute method getElementHtml from parent class * * @return string */ @@ -85,6 +85,8 @@ public function getParentElementHtml() } /** + * Get options. + * * @return array */ public function getOptions() @@ -106,6 +108,8 @@ public function getOptions() } /** + * Is disabled field. + * * @return bool */ public function isDisabledField() @@ -118,6 +122,8 @@ public function isDisabledField() } /** + * Get product. + * * @return mixed */ public function getProduct() @@ -129,6 +135,8 @@ public function getProduct() } /** + * Get extended element. + * * @param string $switchAttributeCode * @return \Magento\Framework\Data\Form\Element\Select * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php index 23fc2026ab111..82a0086ad67ec 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php @@ -100,6 +100,8 @@ public function getChildren($item) } /** + * Check if item can be shipped separately + * * @param mixed $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -136,6 +138,8 @@ public function isShipmentSeparately($item = null) } /** + * Check if child items calculated + * * @param mixed $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -174,6 +178,8 @@ public function isChildCalculated($item = null) } /** + * Retrieve selection attributes values + * * @param mixed $item * @return mixed|null */ @@ -191,6 +197,8 @@ public function getSelectionAttributes($item) } /** + * Retrieve order item options array + * * @return array */ public function getOrderOptions() @@ -212,6 +220,8 @@ public function getOrderOptions() } /** + * Retrieve order item + * * @return mixed */ public function getOrderItem() @@ -223,6 +233,8 @@ public function getOrderItem() } /** + * Get html info for item + * * @param mixed $item * @return string */ @@ -245,6 +257,8 @@ public function getValueHtml($item) } /** + * Check if we can show price info for this item + * * @param object $item * @return bool */ diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php index 7c63af0bd0e2e..7c5a64ca0232f 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php @@ -6,6 +6,8 @@ namespace Magento\Bundle\Block\Catalog\Product\View\Type\Bundle; +use Magento\Catalog\Model\Product; + /** * Bundle option renderer * @api @@ -169,7 +171,7 @@ protected function assignSelection(\Magento\Bundle\Model\Option $option, $select { if (is_array($selectionId)) { $this->_selectedOptions = $selectionId; - } else if ($selectionId && $option->getSelectionById($selectionId)) { + } elseif ($selectionId && $option->getSelectionById($selectionId)) { $this->_selectedOptions = $selectionId; } elseif (!$option->getRequired()) { $this->_selectedOptions = 'None'; @@ -179,7 +181,7 @@ protected function assignSelection(\Magento\Bundle\Model\Option $option, $select /** * Define if selection is selected * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @return bool */ public function isSelected($selection) @@ -219,7 +221,7 @@ protected function _getSelectedQty() /** * Get product model * - * @return \Magento\Catalog\Model\Product + * @return Product */ public function getProduct() { @@ -232,7 +234,7 @@ public function getProduct() /** * Get bundle option price title. * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @param bool $includeContainer * @return string */ @@ -254,7 +256,7 @@ public function getSelectionQtyTitlePrice($selection, $includeContainer = true) /** * Get price for selection product * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @return int|float */ public function getSelectionPrice($selection) @@ -277,7 +279,7 @@ public function getSelectionPrice($selection) /** * Get title price for selection product * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @param bool $includeContainer * @return string */ @@ -299,7 +301,7 @@ public function getSelectionTitlePrice($selection, $includeContainer = true) */ public function setValidationContainer($elementId, $containerId) { - return; + return ''; } /** @@ -318,7 +320,7 @@ public function setOption(\Magento\Bundle\Model\Option $option) /** * Format price string * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @param bool $includeContainer * @return string */ diff --git a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php index c75ebc700603b..863f273225693 100644 --- a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php +++ b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php @@ -69,6 +69,7 @@ public function __construct( /** * Overloaded method for getting list of bundle options + * * Caches result in quote item, because it can be used in cart 'recent view' and on same page in cart checkout * * @return array @@ -88,7 +89,7 @@ public function getMessages() $messages = []; $quoteItem = $this->getItem(); - // Add basic messages occuring during this page load + // Add basic messages occurring during this page load $baseMessages = $quoteItem->getMessage(false); if ($baseMessages) { foreach ($baseMessages as $message) { diff --git a/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php new file mode 100644 index 0000000000000..058b3a981b52f --- /dev/null +++ b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Block\DataProviders; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Pricing\Price\TierPrice; +use Magento\Framework\Pricing\Render; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\View\LayoutInterface; + +/** + * Provides additional data for bundle options + */ +class OptionPriceRenderer implements ArgumentInterface +{ + /** + * Parent layout of the block + * + * @var LayoutInterface + */ + private $layout; + + /** + * @param LayoutInterface $layout + */ + public function __construct(LayoutInterface $layout) + { + $this->layout = $layout; + } + + /** + * Format tier price string + * + * @param Product $selection + * @param array $arguments + * @return string + */ + public function renderTierPrice(Product $selection, array $arguments = []): string + { + if (!array_key_exists('zone', $arguments)) { + $arguments['zone'] = Render::ZONE_ITEM_OPTION; + } + + $priceHtml = ''; + + /** @var Render $priceRender */ + $priceRender = $this->layout->getBlock('product.price.render.default'); + if ($priceRender !== false) { + $priceHtml = $priceRender->render( + TierPrice::PRICE_CODE, + $selection, + $arguments + ); + } + + return $priceHtml; + } +} diff --git a/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php b/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php new file mode 100644 index 0000000000000..499f0cd2ca9c5 --- /dev/null +++ b/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\Plugin\Frontend; + +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Model\Product as CatalogProduct; + +/** + * Add child identities to product identities on storefront. + */ +class Product +{ + /** + * @var Type + */ + private $type; + + /** + * @param Type $type + */ + public function __construct(Type $type) + { + $this->type = $type; + } + + /** + * Add child identities to product identities + * + * @param CatalogProduct $product + * @param array $identities + * @return array + */ + public function afterGetIdentities(CatalogProduct $product, array $identities): array + { + foreach ($this->type->getChildrenIds($product->getEntityId()) as $childIds) { + foreach ($childIds as $childId) { + $identities[] = CatalogProduct::CACHE_TAG . '_' . $childId; + } + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php index 00b6b2d7a3f5a..0e75ad2dc828c 100644 --- a/app/code/Magento/Bundle/Model/Product/Price.php +++ b/app/code/Magento/Bundle/Model/Product/Price.php @@ -11,8 +11,11 @@ use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; /** + * Bundle product type price model + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Price extends \Magento\Catalog\Model\Product\Type\Price @@ -180,9 +183,9 @@ protected function getBundleSelectionIds(\Magento\Catalog\Model\Product $product /** * Get product final price * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float */ public function getFinalPrice($qty, $product) { @@ -207,9 +210,9 @@ public function getFinalPrice($qty, $product) * Returns final price of a child product * * @param \Magento\Catalog\Model\Product $product - * @param float $productQty + * @param float $productQty * @param \Magento\Catalog\Model\Product $childProduct - * @param float $childProductQty + * @param float $childProductQty * @return float */ public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty) @@ -220,10 +223,10 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP /** * Retrieve Price considering tier price * - * @param \Magento\Catalog\Model\Product $product - * @param string|null $which - * @param bool|null $includeTax - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $product + * @param string|null $which + * @param bool|null $includeTax + * @param bool $takeTierPrice * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -255,67 +258,68 @@ public function getTotalPrices($product, $which = null, $includeTax = null, $tak foreach ($options as $option) { /* @var $option \Magento\Bundle\Model\Option */ $selections = $option->getSelections(); - if ($selections) { - $selectionMinimalPrices = []; - $selectionMaximalPrices = []; - - foreach ($option->getSelections() as $selection) { - /* @var $selection \Magento\Bundle\Model\Selection */ - if (!$selection->isSalable()) { - /** - * @todo CatalogInventory Show out of stock Products - */ - continue; - } - - $qty = $selection->getSelectionQty(); - - $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; - - $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice( - $item, - $this->getSelectionFinalTotalPrice( - $product, - $selection, - 1, - $qty, - true, - $takeTierPrice - ), - $includeTax - ); - $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice( - $item, - $this->getSelectionFinalTotalPrice( - $product, - $selection, - 1, - null, - true, - $takeTierPrice - ), - $includeTax + if (empty($selections)) { + continue; + } + $selectionMinimalPrices = []; + $selectionMaximalPrices = []; + + foreach ($option->getSelections() as $selection) { + /* @var $selection \Magento\Bundle\Model\Selection */ + if (!$selection->isSalable()) { + /** + * @todo CatalogInventory Show out of stock Products + */ + continue; + } + + $qty = $selection->getSelectionQty(); + + $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; + + $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice( + $item, + $this->getSelectionFinalTotalPrice( + $product, + $selection, + 1, + $qty, + true, + $takeTierPrice + ), + $includeTax + ); + $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice( + $item, + $this->getSelectionFinalTotalPrice( + $product, + $selection, + 1, + null, + true, + $takeTierPrice + ), + $includeTax + ); + } + + if (count($selectionMinimalPrices)) { + $selMinPrice = min($selectionMinimalPrices); + if ($option->getRequired()) { + $minimalPrice += $selMinPrice; + $minPriceFounded = true; + } elseif (true !== $minPriceFounded) { + $selMinPrice += $minimalPrice; + $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min( + $minPriceFounded, + $selMinPrice ); } - if (count($selectionMinimalPrices)) { - $selMinPrice = min($selectionMinimalPrices); - if ($option->getRequired()) { - $minimalPrice += $selMinPrice; - $minPriceFounded = true; - } elseif (true !== $minPriceFounded) { - $selMinPrice += $minimalPrice; - $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min( - $minPriceFounded, - $selMinPrice - ); - } - - if ($option->isMultiSelection()) { - $maximalPrice += array_sum($selectionMaximalPrices); - } else { - $maximalPrice += max($selectionMaximalPrices); - } + if ($option->isMultiSelection()) { + $maximalPrice += array_sum($selectionMaximalPrices); + } else { + $maximalPrice += max($selectionMaximalPrices); } } } @@ -338,23 +342,25 @@ public function getTotalPrices($product, $which = null, $includeTax = null, $tak $prices[] = $valuePrice; } - if (count($prices)) { - if ($customOption->getIsRequire()) { - $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax); - } - - $multiTypes = [ - \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, - \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, - ]; - - if (in_array($customOption->getType(), $multiTypes)) { - $maximalValue = array_sum($prices); - } else { - $maximalValue = max($prices); - } - $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax); + if (empty($prices)) { + continue; + } + + if ($customOption->getIsRequire()) { + $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax); + } + + $multiTypes = [ + \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, + ]; + + if (in_array($customOption->getType(), $multiTypes)) { + $maximalValue = array_sum($prices); + } else { + $maximalValue = max($prices); } + $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax); } else { $valuePrice = $customOption->getPrice(true); @@ -402,8 +408,8 @@ public function getOptions($product) * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float|null $selectionQty - * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity + * @param float|null $selectionQty + * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity * @return float * * @see \Magento\Bundle\Model\Product\Price::getSelectionFinalTotalPrice() @@ -418,7 +424,7 @@ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQ * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $qty + * @param float $qty * @return float */ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null) @@ -427,15 +433,14 @@ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qt } /** - * Calculate final price of selection - * with take into account tier price + * Calculate final price of selection with take into account tier price * - * @param \Magento\Catalog\Model\Product $bundleProduct - * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $bundleQty - * @param float $selectionQty - * @param bool $multiplyQty - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $bundleProduct + * @param \Magento\Catalog\Model\Product $selectionProduct + * @param float $bundleQty + * @param float $selectionQty + * @param bool $multiplyQty + * @param bool $takeTierPrice * @return float */ public function getSelectionFinalTotalPrice( @@ -454,7 +459,11 @@ public function getSelectionFinalTotalPrice( } if ($bundleProduct->getPriceType() == self::PRICE_TYPE_DYNAMIC) { - $price = $selectionProduct->getFinalPrice($takeTierPrice ? $selectionQty : 1); + $totalQty = $bundleQty * $selectionQty; + if (!$takeTierPrice || $totalQty === 0) { + $totalQty = 1; + } + $price = $selectionProduct->getFinalPrice($totalQty); } else { if ($selectionProduct->getSelectionPriceType()) { // percent @@ -485,10 +494,10 @@ public function getSelectionFinalTotalPrice( /** * Apply tier price for bundle * - * @param \Magento\Catalog\Model\Product $product - * @param float $qty - * @param float $finalPrice - * @return float + * @param \Magento\Catalog\Model\Product $product + * @param float $qty + * @param float $finalPrice + * @return float */ protected function _applyTierPrice($product, $qty, $finalPrice) { @@ -509,9 +518,9 @@ protected function _applyTierPrice($product, $qty, $finalPrice) /** * Get product tier price by qty * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float|array + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -605,11 +614,11 @@ public function getTierPrice($qty, $product) /** * Calculate and apply special price * - * @param float $finalPrice - * @param float $specialPrice + * @param float $finalPrice + * @param float $specialPrice * @param string $specialPriceFrom * @param string $specialPriceTo - * @param mixed $store + * @param mixed $store * @return float */ public function calculateSpecialPrice( @@ -634,7 +643,7 @@ public function calculateSpecialPrice( * * @param /Magento/Catalog/Model/Product $bundleProduct * @param float|string $price - * @param int $bundleQty + * @param int $bundleQty * @return float */ public function getLowestPrice($bundleProduct, $price, $bundleQty = 1) diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index 641f4490874da..2dc519dbf1540 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -13,6 +13,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\ArrayUtils; /** * Bundle Type Model @@ -160,6 +161,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $selectionCollectionFilterApplier; + /** + * @var ArrayUtils + */ + private $arrayUtility; + /** * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig @@ -185,6 +191,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier + * @param ArrayUtils|null $arrayUtility * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -212,7 +219,8 @@ public function __construct( \Magento\CatalogInventory\Api\StockStateInterface $stockState, Json $serializer = null, MetadataPool $metadataPool = null, - SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null + SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, + ArrayUtils $arrayUtility = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -232,6 +240,7 @@ public function __construct( $this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier ?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class); + $this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class); parent::__construct( $catalogProductOption, @@ -673,7 +682,7 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p $options ); - $selectionIds = $this->multiToFlatArray($options); + $selectionIds = array_values($this->arrayUtility->flatten($options)); // If product has not been configured yet then $selections array should be empty if (!empty($selectionIds)) { $selections = $this->getSelectionsByIds($selectionIds, $product); @@ -814,26 +823,6 @@ private function recursiveIntval(array $array) return $array; } - /** - * Convert multi dimensional array to flat - * - * @param array $array - * @return int[] - */ - private function multiToFlatArray(array $array) - { - $flatArray = []; - foreach ($array as $value) { - if (is_array($value)) { - $flatArray = array_merge($flatArray, $this->multiToFlatArray($value)); - } else { - $flatArray[] = $value; - } - } - - return $flatArray; - } - /** * Retrieve message for specify option(s) * diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php new file mode 100644 index 0000000000000..701def7fc13d8 --- /dev/null +++ b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Setup\Patch\Data; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Setup\EavSetup; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; + +/** + * Class UpdateBundleRelatedEntityTypes + * + * @package Magento\Bundle\Setup\Patch + */ +class UpdateBundleRelatedEntityTypes implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var EavSetupFactory + */ + private $eavSetupFactory; + + /** + * UpdateBundleRelatedEntityTypes constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + * @param EavSetupFactory $eavSetupFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->eavSetupFactory = $eavSetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); + + $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); + $eavSetup->addAttributeGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSetId, + 'Bundle Items', + 16 + ); + $this->upgradePriceType($eavSetup); + $this->upgradeSkuType($eavSetup); + $this->upgradeWeightType($eavSetup); + $this->upgradeShipmentType($eavSetup); + } + + /** + * Upgrade Dynamic Price attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradePriceType(EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'price_type', + 'frontend_input', + 'boolean', + 31 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'price_type', + 'frontend_label', + 'Dynamic Price' + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'price_type', 'default_value', 0); + } + + /** + * Upgrade Dynamic Sku attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradeSkuType(EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'sku_type', + 'frontend_input', + 'boolean', + 21 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'sku_type', + 'frontend_label', + 'Dynamic SKU' + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'default_value', 0); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'is_visible', 1); + } + + /** + * Upgrade Dynamic Weight attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradeWeightType(EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'weight_type', + 'frontend_input', + 'boolean', + 71 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'weight_type', + 'frontend_label', + 'Dynamic Weight' + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'default_value', 0); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'is_visible', 1); + } + + /** + * Upgrade Ship Bundle Items attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradeShipmentType(EavSetup $eavSetup) + { + $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); + $eavSetup->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSetId, + 'Bundle Items', + 'shipment_type', + 1 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'shipment_type', + 'frontend_input', + 'select' + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'shipment_type', + 'frontend_label', + 'Ship Bundle Items' + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'shipment_type', + 'source_model', + \Magento\Bundle\Model\Product\Attribute\Source\Shipment\Type::class + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'default_value', 0); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'is_visible', 1); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + ApplyAttributesUpdate::class, + ]; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.2'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php deleted file mode 100644 index 44647ea76a1c2..0000000000000 --- a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php +++ /dev/null @@ -1,204 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Bundle\Setup\Patch\Data; - -use Magento\Eav\Setup\EavSetupFactory; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Framework\Setup\Patch\DataPatchInterface; -use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Eav\Setup\EavSetup; - -/** - * Class UpdateBundleRelatedEntityTytpes - * @package Magento\Bundle\Setup\Patch - */ -class UpdateBundleRelatedEntityTytpes implements DataPatchInterface, PatchVersionInterface -{ - /** - * @var ModuleDataSetupInterface - */ - private $moduleDataSetup; - - /** - * @var EavSetupFactory - */ - private $eavSetupFactory; - - /** - * UpdateBundleRelatedEntityTytpes constructor. - * @param ModuleDataSetupInterface $moduleDataSetup - * @param EavSetupFactory $eavSetupFactory - */ - public function __construct( - ModuleDataSetupInterface $moduleDataSetup, - \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory - ) { - $this->moduleDataSetup = $moduleDataSetup; - $this->eavSetupFactory = $eavSetupFactory; - } - - /** - * {@inheritdoc} - */ - public function apply() - { - /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ - $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); - - $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); - $eavSetup->addAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeSetId, - 'Bundle Items', - 16 - ); - $this->upgradePriceType($eavSetup); - $this->upgradeSkuType($eavSetup); - $this->upgradeWeightType($eavSetup); - $this->upgradeShipmentType($eavSetup); - } - - /** - * Upgrade Dynamic Price attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradePriceType(EavSetup $eavSetup) - { - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'price_type', - 'frontend_input', - 'boolean', - 31 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'price_type', - 'frontend_label', - 'Dynamic Price' - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'price_type', 'default_value', 0); - } - - /** - * Upgrade Dynamic Sku attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradeSkuType(EavSetup $eavSetup) - { - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'sku_type', - 'frontend_input', - 'boolean', - 21 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'sku_type', - 'frontend_label', - 'Dynamic SKU' - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'default_value', 0); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'is_visible', 1); - } - - /** - * Upgrade Dynamic Weight attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradeWeightType(EavSetup $eavSetup) - { - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'weight_type', - 'frontend_input', - 'boolean', - 71 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'weight_type', - 'frontend_label', - 'Dynamic Weight' - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'default_value', 0); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'is_visible', 1); - } - - /** - * Upgrade Ship Bundle Items attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradeShipmentType(EavSetup $eavSetup) - { - $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); - $eavSetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeSetId, - 'Bundle Items', - 'shipment_type', - 1 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'shipment_type', - 'frontend_input', - 'select' - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'shipment_type', - 'frontend_label', - 'Ship Bundle Items' - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'shipment_type', - 'source_model', - \Magento\Bundle\Model\Product\Attribute\Source\Shipment\Type::class - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'default_value', 0); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'is_visible', 1); - } - - /** - * {@inheritdoc} - */ - public static function getDependencies() - { - return [ - ApplyAttributesUpdate::class, - ]; - } - - /** - * {@inheritdoc} - */ - public static function getVersion() - { - return '2.0.2'; - } - - /** - * {@inheritdoc} - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml index ad9a8253e910c..4cd16320d3d78 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml @@ -106,4 +106,62 @@ <requiredEntity createDataKey="simpleProduct4"/> </createData> </actionGroup> + <actionGroup name="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup"> + <arguments> + <argument name="productName" defaultValue="Api Dynamic Bundle Product" type="string"/> + </arguments> + <!-- Create simple products --> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">20</field> + </createData> + <!-- Create Bundle product --> + <createData entity="ApiBundleProduct" stepKey="createBundleProduct"> + <field key="name">{{productName}}</field> + </createData> + <createData entity="DropDownBundleOption" stepKey="createDropDownBundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="title">Drop-down Option</field> + </createData> + <createData entity="RadioButtonsOption" stepKey="createBundleRadioButtonsOption"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="title">Radio Buttons Option</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleCheckboxOption"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="title">Checkbox Option</field> + </createData> + <createData entity="ApiBundleLink" stepKey="linkCheckboxOptionToProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleCheckboxOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkCheckboxOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleCheckboxOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkDropDownOptionToProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createDropDownBundleOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkDropDownOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createDropDownBundleOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkRadioButtonsOptionToProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleRadioButtonsOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkRadioButtonsOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleRadioButtonsOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml index 72e729111948f..d86d720ed7f5d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml @@ -56,4 +56,74 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="50" stepKey="fillQuantity1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '1')}}" userInput="50" stepKey="fillQuantity2"/> </actionGroup> + + <actionGroup name="addBundleOptionWithOneProduct" extends="addBundleOptionWithTwoProducts"> + <remove keyForRemoval="openProductFilters2"/> + <remove keyForRemoval="fillProductSkuFilter2"/> + <remove keyForRemoval="clickApplyFilters2"/> + <remove keyForRemoval="waitForFilteredGridLoad2"/> + <remove keyForRemoval="selectProduct2"/> + <remove keyForRemoval="selectProduct2"/> + <remove keyForRemoval="fillQuantity1"/> + <remove keyForRemoval="fillQuantity2"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="1" stepKey="fillQuantity" after="clickAddButton1"/> + </actionGroup> + + <actionGroup name="addBundleOptionWithTreeProducts" extends="addBundleOptionWithTwoProducts"> + <arguments> + <argument name="prodTreeSku" type="string"/> + </arguments> + <remove keyForRemoval="fillQuantity1"/> + <remove keyForRemoval="fillQuantity2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters3" after="selectProduct2"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters3" after="clickClearFilters3"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodTreeSku}}" stepKey="fillProductSkuFilter3" after="openProductFilters3"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters3" after="fillProductSkuFilter3"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad3" time="30" after="clickApplyFilters3"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct3" after="waitForFilteredGridLoad3"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="1" stepKey="fillQuantity1" after="clickAddButton1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '1')}}" userInput="1" stepKey="fillQuantity2" after="fillQuantity1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '2')}}" userInput="1" stepKey="fillQuantity3" after="fillQuantity2"/> + </actionGroup> + + <actionGroup name="addBundleOptionWithSixProducts" extends="addBundleOptionWithTwoProducts"> + <arguments> + <argument name="prodTreeSku" type="string"/> + <argument name="prodFourSku" type="string"/> + <argument name="prodFiveSku" type="string"/> + <argument name="prodSixSku" type="string"/> + </arguments> + <remove keyForRemoval="fillQuantity1"/> + <remove keyForRemoval="fillQuantity2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters3" after="selectProduct2"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters3" after="clickClearFilters3"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodTreeSku}}" stepKey="fillProductSkuFilter3" after="openProductFilters3"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters3" after="fillProductSkuFilter3"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad3" time="30" after="clickApplyFilters3"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct3" after="waitForFilteredGridLoad3"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters4" after="selectProduct3"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters4" after="clickClearFilters4"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodFourSku}}" stepKey="fillProductSkuFilter4" after="openProductFilters4"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters4" after="fillProductSkuFilter4"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad4" time="30" after="clickApplyFilters4"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct4" after="clickApplyFilters4"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters5" after="selectProduct4"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters5" after="clickClearFilters5"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodFiveSku}}" stepKey="fillProductSkuFilter5" after="openProductFilters5"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters5" after="fillProductSkuFilter5"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad5" time="30" after="clickApplyFilters5"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct5" after="waitForFilteredGridLoad5"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters6" after="selectProduct5"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters6" after="clickClearFilters6"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodSixSku}}" stepKey="fillProductSkuFilter6" after="openProductFilters6"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters6" after="fillProductSkuFilter6"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad6" time="30" after="clickApplyFilters6"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct6" after="waitForFilteredGridLoad6"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="2" stepKey="fillQuantity1" after="clickAddButton1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '1')}}" userInput="2" stepKey="fillQuantity2" after="fillQuantity1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '2')}}" userInput="2" stepKey="fillQuantity3" after="fillQuantity2"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '3')}}" userInput="2" stepKey="fillQuantity4" after="fillQuantity3"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '4')}}" userInput="2" stepKey="fillQuantity5" after="fillQuantity4"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '5')}}" userInput="2" stepKey="fillQuantity6" after="fillQuantity5"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml index e3ac6483bc7bd..20bde5f87bd7b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml @@ -14,7 +14,8 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> <waitForPageLoad stepKey="WaitForDropDownSEO"/> <!--Fill URL input--> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml index e36730a87b41a..1767db0a00974 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -38,4 +38,24 @@ <waitForElementVisible selector="{{StorefrontMinicartSection.productCount}}" stepKey="waitProductCount"/> <see userInput="You added {{productName}} to your shopping cart." selector="{{StorefrontMessagesSection.success}}" stepKey="seeSuccessMessage"/> </actionGroup> + + <!-- Add Bundle Product to Cart from product Page--> + <actionGroup name="StorefrontAddBundleProductFromProductToCartActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomizeAndAddToCart"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddBundleProductToCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.productCount}}" stepKey="waitProductCount"/> + <see userInput="You added {{productName}} to your shopping cart." selector="{{StorefrontMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Add Bundled Product to Cart with selected multiselect option--> + <actionGroup name="StorefrontAddBundleProductFromProductToCartWithMultiOption" extends="StorefrontAddBundleProductFromProductToCartActionGroup"> + <arguments> + <argument name="optionName" type="string"/> + <argument name="value" type="string"/> + </arguments> + <selectOption selector="{{StorefrontBundledSection.multiselectOptionFourProducts(optionName)}}" userInput="{{value}}" stepKey="selectValue" before="clickAddBundleProductToCart"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 0000000000000..a00f1e367864e --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ +<?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="VerifyProductTypeOrder"> + <seeElement stepKey="seeBundleInOrder" selector="{{AdminProductDropdownOrderSection.bundleProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml new file mode 100644 index 0000000000000..5cd286c0c6aa1 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.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="BundleProductsSummary" type="Quote"> + <data key="subtotal">1,968.00</data> + <data key="shipping">5.00</data> + <data key="total">1,973.00</data> + <data key="shippingMethod">Flat Rate - Fixed</data> + </entity> +</entities> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml index e6866ae74a7e1..256bfd7746957 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml @@ -23,4 +23,12 @@ <data key="attribute_code">price_view</data> <data key="value">0</data> </entity> + <entity name="CustomAttributeFixWeight" type="custom_attribute"> + <data key="attribute_code">weight_type</data> + <data key="value">1</data> + </entity> + <entity name="CustomAttributeFixSku" type="custom_attribute"> + <data key="attribute_code">sku_type</data> + <data key="value">1</data> + </entity> </entities> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index 9dc30e73228d3..0a0c77755fc7a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -31,6 +31,22 @@ <data key="fixedPriceFormatted">$10.00</data> <data key="defaultAttribute">Default</data> </entity> + <entity name="FixedBundleProduct" type="product2"> + <data key="name" unique="suffix">FixedBundleProduct</data> + <data key="sku" unique="suffix">fixed-bundle-product</data> + <data key="type_id">bundle</data> + <data key="attribute_set_id">4</data> + <data key="price">1.23</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">fixed-bundle-product</data> + <requiredEntity type="custom_attribute">CustomAttributeCategoryIds</requiredEntity> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixPrice</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixWeight</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixSku</requiredEntity> + </entity> <entity name="ApiBundleProduct" type="product2"> <data key="name" unique="suffix">Api Bundle Product</data> <data key="sku" unique="suffix">api-bundle-product</data> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 0000000000000..787f7ade8ffab --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.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="AdminProductDropdownOrderSection"> + <element name="bundleProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']]) and not(following-sibling::li[span[@title='Configurable Product']]) and not(following-sibling::li[span[@title='Grouped Product']]) and not(following-sibling::li[span[@title='Virtual Product']])]/span[@title='Bundle Product']"/> + </section> +</sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 814d03c52f4be..516f40ac2e7b7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -22,6 +22,8 @@ <element name="bundleOptionXProductYQuantity" type="input" selector="[name='bundle_options[bundle_options][{{x}}][bundle_selections][{{y}}][selection_qty]']" parameterized="true"/> <element name="addProductsToOption" type="button" selector="[data-index='modal_set']" timeout="30"/> <element name="nthAddProductsToOption" type="button" selector="//tr[{{var}}]//button[@data-index='modal_set']" timeout="30" parameterized="true"/> + <element name="bundlePriceType" type="select" selector="bundle_options[bundle_options][0][bundle_selections][0][selection_price_type]"/> + <element name="bundlePriceValue" type="input" selector="bundle_options[bundle_options][0][bundle_selections][0][selection_price_value]"/> <!--Select"url Key"InputForm--> <element name="urlKey" type="input" selector="//input[@name='product[url_key]']" timeout="30"/> <!--AddSelectedProducts--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index 946992f1efe04..30a7e8b777f3b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -9,6 +9,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontBundledSection"> + <element name="productCheckbox" type="select" selector="//*[@id='customizeTitle']/following-sibling::div[{{arg1}}]//div[{{arg2}}][@class='field choice']/input" parameterized="true"/> + <element name="bundleProductsPrice" type="text" selector="//*[@class='bundle-info']//*[contains(@id,'product-price')]/span"/> <element name="nthBundledOption" type="input" selector=".option:nth-of-type({{numOption}}) .choice:nth-of-type({{numOptionSelect}}) input" parameterized="true"/> <element name="addToCart" type="button" selector="#bundle-slide" timeout="30"/> <element name="addToCartConfigured" type="button" selector="#product-addtocart-button" timeout="30"/> @@ -24,11 +26,14 @@ <element name="bundleProductName" type="text" selector="//*[@id='maincontent']//span[@itemprop='name']"/> <element name="pageNotFound" type="text" selector="//h1[@class='page-title']//span[contains(., 'Whoops, our bad...')]"/> <element name="dropDownOptionOneProducts" type="select" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//select" parameterized="true"/> + <element name="dropDownOptionTierPrices" type="text" selector="//label//span[contains(text(), '{{optionName}}')]/../..//div[@class='control']//div[@class='option-tier-prices']" parameterized="true"/> <element name="productInBundle" type="select" selector="//label//span[contains(text(), '{{productName}}')]" parameterized="true"/> <element name="dropDownOptionOneQuantity" type="input" selector="//span[contains(text(), '{{productName}}')]/../..//input" parameterized="true"/> <element name="radioButtonOptionTwoProducts" type="checkbox" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//div[@class='field choice'][{{productNumber}}]/input" parameterized="true"/> <element name="radioButtonOptionTwoQuantity" type="input" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//div[@class='field qty qty-holder']//input" parameterized="true"/> + <element name="radioButtonOptionLabel" type="text" selector="//label//span[contains(text(), '{{optionName}}')]/../..//div[@class='control']//div[@class='field choice']//label[contains(.,'{{productName}}')]" parameterized="true"/> <element name="checkboxOptionThreeProducts" type="checkbox" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//div[@class='field choice'][{{productNumber}}]/input" parameterized="true"/> + <element name="checkboxOptionLabel" type="text" selector="//label//span[contains(text(), '{{optionName}}')]/../..//div[@class='control']//div[@class='field choice']//label[contains(.,'{{productName}}')]" parameterized="true"/> <element name="multiselectOptionFourProducts" type="multiselect" selector="//label//span[contains(text(), '{{productName}}')]/../..//select[@multiple='multiple']" parameterized="true"/> <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> <element name="currency" type="select" selector="//a[text()='{{arg}}']" parameterized="true"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml index 3c00344697699..c49202f31aefb 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoBundleProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="Bundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml new file mode 100644 index 0000000000000..bc9a3dba9a5f1 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteBundleDynamicProductTest"> + <annotations> + <features value="Bundle"/> + <stories value="Delete products"/> + <title value="Delete Bundle Dynamic Product"/> + <description value="Admin should be able to delete a bundle dynamic product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11016"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createDynamicBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundleProductFilteredBySkuAndName"> + <argument name="product" value="$$createDynamicBundleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createDynamicBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDynamicBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createDynamicBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createDynamicBundleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml new file mode 100644 index 0000000000000..2527dae7eadf8 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteBundleFixedProductTest"> + <annotations> + <features value="Bundle"/> + <stories value="Delete products"/> + <title value="Delete Bundle Fixed Product"/> + <description value="Admin should be able to delete a bundle fixed product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11017"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="FixedBundleProduct" stepKey="createFixedBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundleProductFilteredBySkuAndName"> + <argument name="product" value="$$createFixedBundleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createFixedBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createFixedBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createFixedBundleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml index c0edbf14e894b..2f891fcc8f169 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml @@ -99,8 +99,8 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku2"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed2"/> - <waitForPageLoad stepKey="WaitForDropDownSEO"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> <!--Fill URL input--> <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml index e3cb68b6664e2..d050c5443d1fe 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoBundleProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="Bundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml index 0b220efaad49f..52bce67600888 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchBundleByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="Bundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml index 574c0dccdb07f..c922b981aecd9 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml @@ -31,6 +31,10 @@ <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <!-- Delete the bundled product we created in the test body --> + <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> <!--Go to bundle product creation page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index 5b2b771434b73..ff192538637ef 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -60,15 +60,7 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> - <fillField selector="{{AdminProductFormBundleSection.productName}}" userInput="{{BundleProduct.name}}" stepKey="fillProductName"/> - <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> - - <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed"/> - <waitForPageLoad stepKey="WaitForDropDownSEO"/> - - <!--Fill URL input--> - <fillField userInput="{{BundleProduct.urlKey}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension"/> + <actionGroup ref="AncillaryPrepBundleProduct" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> @@ -104,7 +96,8 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku2"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed2"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> <waitForPageLoad stepKey="WaitForDropDownSEO2"/> <!--Fill URL input--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml new file mode 100644 index 0000000000000..a1630128638d9 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml @@ -0,0 +1,140 @@ +<?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="StorefrontAddBundleOptionsToCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="MAGETWO-95813: Only two bundle options are added to the cart"/> + <title value="Checking adding of bundle options to the cart"/> + <description value="Verifying adding of bundle options to the cart"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95933"/> + <group value="Bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct5"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct6"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct7"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct8"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct9"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct10"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> + <deleteData createDataKey="simpleProduct6" stepKey="deleteSimpleProduct6"/> + <deleteData createDataKey="simpleProduct7" stepKey="deleteSimpleProduct7"/> + <deleteData createDataKey="simpleProduct8" stepKey="deleteSimpleProduct8"/> + <deleteData createDataKey="simpleProduct9" stepKey="deleteSimpleProduct9"/> + <deleteData createDataKey="simpleProduct10" stepKey="deleteSimpleProduct10"/> + <!--delete created bundle product--> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Start creating a bundle product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!-- Add Option One, a "Checkbox" type option, with tree products --> + <actionGroup ref="addBundleOptionWithTreeProducts" stepKey="addBundleOptionWithTreeProducts"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="prodTreeSku" value="$$simpleProduct3.sku$$"/> + <argument name="optionTitle" value="Option One"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + + <!-- Add Option Two, a "Radio Buttons" type option, with one product --> + <actionGroup ref="addBundleOptionWithOneProduct" stepKey="addBundleOptionWithOneProduct"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$$simpleProduct4.sku$$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option Two"/> + <argument name="inputType" value="radio"/> + </actionGroup> + + <!-- Add Option Tree, a "Checkbox" type option, with six products --> + <actionGroup ref="addBundleOptionWithSixProducts" stepKey="addBundleOptionWithSixProducts"> + <argument name="x" value="2"/> + <argument name="n" value="3"/> + <argument name="prodOneSku" value="$$simpleProduct5.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct6.sku$$"/> + <argument name="prodTreeSku" value="$$simpleProduct7.sku$$"/> + <argument name="prodFourSku" value="$$simpleProduct8.sku$$"/> + <argument name="prodFiveSku" value="$$simpleProduct9.sku$$"/> + <argument name="prodSixSku" value="$$simpleProduct10.sku$$"/> + <argument name="optionTitle" value="Option Tree"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + + <!-- Save product--> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to Storefront and open Bundle Product page--> + <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + + <!--Click "Customize and Add to Cart" button--> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!--Assert Bundle Product Price--> + <grabTextFrom selector="{{StorefrontBundledSection.bundleProductsPrice}}" stepKey="grabProductsPrice"/> + <assertEquals expected='$123.00' expectedType="string" actual="$grabProductsPrice" message="ExpectedPrice" stepKey="assertBundleProductPrice"/> + + <!--Chose all products from 1st & 3rd options --> + <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.productCheckbox('1','1')}}"/> + <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.productCheckbox('1','2')}}"/> + <click stepKey="selectProduct3" selector="{{StorefrontBundledSection.productCheckbox('1','3')}}"/> + <click stepKey="selectProduct5" selector="{{StorefrontBundledSection.productCheckbox('3','1')}}"/> + <click stepKey="selectProduct6" selector="{{StorefrontBundledSection.productCheckbox('3','2')}}"/> + <click stepKey="selectProduct7" selector="{{StorefrontBundledSection.productCheckbox('3','3')}}"/> + <click stepKey="selectProduct8" selector="{{StorefrontBundledSection.productCheckbox('3','4')}}"/> + <click stepKey="selectProduct9" selector="{{StorefrontBundledSection.productCheckbox('3','5')}}"/> + <click stepKey="selectProduct10" selector="{{StorefrontBundledSection.productCheckbox('3','6')}}"/> + + <!--Click "Add to Cart" button--> + <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickAddBundleProductToCart"/> + <waitForPageLoad time="30" stepKey="waitForAddBundleProductPageLoad"/> + + <!--Click "mini cart" icon--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <waitForPageLoad stepKey="waitForDetailsOpen"/> + + <!--Check all products and Cart Subtotal --> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssert" after="waitForDetailsOpen"> + <argument name="subtotal" value="1,968.00"/> + <argument name="shipping" value="5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="1,973.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml new file mode 100644 index 0000000000000..4c39cbc4ab0a4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.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="StorefrontCheckBundleProductOptionTierPrices"> + <annotations> + <features value="Bundle"/> + <stories value="View bundle products"/> + <title value="Check tier prices for bundle options"/> + <testCaseId value="MAGETWO-98968"/> + <useCaseId value="MAGETWO-98603"/> + <group value="catalog"/> + <group value="bundle"/> + </annotations> + <before> + <!-- Create Dynamic Bundle product --> + <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> + + <!-- Add tier prices to simple products --> + <!-- Simple product 1 --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct1CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct1"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct1"> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="5"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + </actionGroup> + <!-- Simple product 2 --> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct2CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct2"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct2"> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="7"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="25"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutAsAdmin"/> + + <!-- Run reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2CreateBundleProduct" stepKey="deleteSimpleProduct2"/> + </after> + + <!-- Go to storefront product page --> + <amOnPage url="{{StorefrontProductPage.url($$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToBundleProductPage"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!--"Drop-down" type option--> + <!-- Check Tier Prices for product 1 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct1"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct1"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="DropDownTierPriceTextProduct1"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct2"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct2"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="dropDownTierPriceTextProduct2"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">dropDownTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Radio Buttons" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Checkbox" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct2</actualResult> + </assertContains> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php new file mode 100644 index 0000000000000..1af73bafc6256 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Block\DataProviders; + +use Magento\Bundle\Block\DataProviders\OptionPriceRenderer; +use Magento\Catalog\Model\Product; +use Magento\Framework\Pricing\Render; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class to test additional data for bundle options + */ +class OptionPriceRendererTest extends TestCase +{ + /** + * @var LayoutInterface|MockObject + */ + private $layoutMock; + + /** + * @var OptionPriceRenderer + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->layoutMock = $this->createMock( + LayoutInterface::class + ); + + $this->renderer = $objectManager->getObject( + OptionPriceRenderer::class, + ['layout' => $this->layoutMock] + ); + } + + /** + * Test to render Tier price html + * + * @return void + */ + public function testRenderTierPrice(): void + { + $expectedHtml = 'tier price html'; + $expectedArguments = ['zone' => Render::ZONE_ITEM_OPTION]; + + $productMock = $this->createMock(Product::class); + + $priceRenderer = $this->createPartialMock(BlockInterface::class, ['toHtml', 'render']); + $priceRenderer->expects($this->once()) + ->method('render') + ->with('tier_price', $productMock, $expectedArguments) + ->willReturn($expectedHtml); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn($priceRenderer); + + $this->assertEquals( + $expectedHtml, + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } + + /** + * Test to render Tier price html when render block is not exists + * + * @return void + */ + public function testRenderTierPriceNotExist(): void + { + $productMock = $this->createMock(Product::class); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn(false); + + $this->assertEquals( + '', + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php new file mode 100644 index 0000000000000..ee08618eab5dd --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Test\Unit\Model\Plugin\Frontend; + +use Magento\Bundle\Model\Plugin\Frontend\Product as ProductPlugin; +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Model\Product; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class ProductTest extends \PHPUnit\Framework\TestCase +{ + /** @var \Magento\Bundle\Model\Plugin\Product */ + private $plugin; + + /** @var MockObject|Type */ + private $type; + + /** @var MockObject|\Magento\Catalog\Model\Product */ + private $product; + + protected function setUp() + { + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityId']) + ->getMock(); + + $this->type = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->setMethods(['getChildrenIds']) + ->getMock(); + + $this->plugin = new ProductPlugin($this->type); + } + + public function testAfterGetIdentities() + { + $baseIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + ]; + $id = 12345; + $childIds = [ + 1 => [1, 2, 5, 100500], + 12 => [7, 22, 45, 24612] + ]; + $expectedIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + Product::CACHE_TAG . '_' . 1, + Product::CACHE_TAG . '_' . 2, + Product::CACHE_TAG . '_' . 5, + Product::CACHE_TAG . '_' . 100500, + Product::CACHE_TAG . '_' . 7, + Product::CACHE_TAG . '_' . 22, + Product::CACHE_TAG . '_' . 45, + Product::CACHE_TAG . '_' . 24612, + ]; + $this->product->expects($this->once()) + ->method('getEntityId') + ->will($this->returnValue($id)); + $this->type->expects($this->once()) + ->method('getChildrenIds') + ->with($id) + ->will($this->returnValue($childIds)); + $identities = $this->plugin->afterGetIdentities($this->product, $baseIdentities); + $this->assertEquals($expectedIdentities, $identities); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index 59f7f008ed3ee..9d7629c6f0a41 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -15,6 +15,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\ArrayUtils; /** * Class TypeTest @@ -87,6 +88,11 @@ class TypeTest extends \PHPUnit\Framework\TestCase */ private $serializer; + /** + * @var ArrayUtils|\PHPUnit_Framework_MockObject_MockObject + */ + private $arrayUtility; + /** * @return void */ @@ -159,6 +165,11 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->arrayUtility = $this->getMockBuilder(ArrayUtils::class) + ->setMethods(['flatten']) + ->disableOriginalConstructor() + ->getMock(); + $objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectHelper->getObject( \Magento\Bundle\Model\Product\Type::class, @@ -175,6 +186,7 @@ protected function setUp() 'priceCurrency' => $this->priceCurrency, 'serializer' => $this->serializer, 'metadataPool' => $this->metadataPool, + 'arrayUtility' => $this->arrayUtility ] ); } @@ -421,6 +433,8 @@ function ($key) use ($optionCollection, $selectionCollection) { return $resultValue; } ); + $bundleOptions = [3 => 5]; + $product->expects($this->any()) ->method('getId') ->willReturn(333); @@ -438,9 +452,7 @@ function ($key) use ($optionCollection, $selectionCollection) { ->with($selectionCollection, true, true); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -491,6 +503,9 @@ function ($key) use ($optionCollection, $selectionCollection) { $option->expects($this->once()) ->method('getTitle') ->willReturn('Title for option'); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $buyRequest->expects($this->once()) ->method('getBundleOptionQty') ->willReturn([3 => 5]); @@ -653,6 +668,8 @@ function ($key) use ($optionCollection, $selectionCollection) { return $resultValue; } ); + $bundleOptions = [3 => 5]; + $product->expects($this->any()) ->method('getId') ->willReturn(333); @@ -672,7 +689,10 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('setStoreFilter'); $buyRequest->expects($this->once()) ->method('getBundleOption') - ->willReturn([3 => 5]); + ->willReturn($bundleOptions); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -890,9 +910,10 @@ function ($key) use ($optionCollection, $selectionCollection) { ->with($selectionCollection, true, true); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + + $bundleOptions = [3 => 5]; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); + $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -943,6 +964,9 @@ function ($key) use ($optionCollection, $selectionCollection) { $option->expects($this->once()) ->method('getTitle') ->willReturn('Title for option'); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $buyRequest->expects($this->once()) ->method('getBundleOptionQty') ->willReturn([3 => 5]); @@ -1053,13 +1077,15 @@ function ($key) use ($optionCollection) { ->willReturn(333); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([]); + + $bundleOptions = []; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); $buyRequest->expects($this->once()) ->method('getBundleOptionQty') ->willReturn([3 => 5]); + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $result = $this->model->prepareForCartAdvanced($buyRequest, $product, 'single'); $this->assertEquals([$product], $result); } @@ -1165,9 +1191,12 @@ function ($key) use ($optionCollection, $selectionCollection) { ->with($selectionCollection, true, true); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + + $bundleOptions = [3 => 5]; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $selectionCollection->expects($this->at(0)) ->method('getItems') ->willReturn([$selection]); @@ -1289,9 +1318,12 @@ function ($key) use ($optionCollection, $selectionCollection) { ->willReturn($option); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + + $bundleOptions = [3 => 5]; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php index f38dfc5538cf3..3e60e057fe62b 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php @@ -6,6 +6,7 @@ namespace Magento\Bundle\Test\Unit\Pricing\Price; use \Magento\Bundle\Pricing\Price\SpecialPrice; +use Magento\Store\Api\Data\WebsiteInterface; class SpecialPriceTest extends \PHPUnit\Framework\TestCase { @@ -77,12 +78,6 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva ->method('getSpecialPrice') ->will($this->returnValue($specialPrice)); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $this->saleable->expects($this->once()) - ->method('getStore') - ->will($this->returnValue($store)); $this->saleable->expects($this->once()) ->method('getSpecialFromDate') ->will($this->returnValue($specialFromDate)); @@ -92,7 +87,7 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva $this->localeDate->expects($this->once()) ->method('isScopeDateInInterval') - ->with($store, $specialFromDate, $specialToDate) + ->with(WebsiteInterface::ADMIN_CODE, $specialFromDate, $specialToDate) ->will($this->returnValue($isScopeDateInInterval)); $this->priceCurrencyMock->expects($this->never()) diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index 98fd96c52ccd9..ad6fc12712c17 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -14,6 +14,7 @@ use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; use Magento\Ui\Component\Form; +use Magento\Ui\Component\Form\Fieldset; use Magento\Ui\Component\Modal; /** @@ -69,13 +70,26 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) { $meta = $this->removeFixedTierPrice($meta); - $path = $this->arrayManager->findPath(static::CODE_BUNDLE_DATA, $meta, null, 'children'); + + $groupCode = static::CODE_BUNDLE_DATA; + $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); + if (empty($path)) { + $meta[$groupCode]['children'] = []; + $meta[$groupCode]['arguments']['data']['config'] = [ + 'componentType' => Fieldset::NAME, + 'label' => __('Bundle Items'), + 'collapsible' => true + ]; + + $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); + } $meta = $this->arrayManager->merge( $path, @@ -220,7 +234,7 @@ private function removeFixedTierPrice(array $meta) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { diff --git a/app/code/Magento/Bundle/etc/frontend/di.xml b/app/code/Magento/Bundle/etc/frontend/di.xml index 54f6d3b4b0f42..fc820ff87a129 100644 --- a/app/code/Magento/Bundle/etc/frontend/di.xml +++ b/app/code/Magento/Bundle/etc/frontend/di.xml @@ -13,4 +13,7 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product"> + <plugin name="bundle" type="Magento\Bundle\Model\Plugin\Frontend\Product" sortOrder="100" /> + </type> </config> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml index 224cd71538b7b..a770ae864a74c 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml @@ -20,9 +20,9 @@ $isElementReadonly = $block->getElement() ->getReadonly(); ?> -<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) { ?> +<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)): ?> <div class="<?= /* @escapeNotVerified */ $attributeCode ?> "><?= /* @escapeNotVerified */ $elementHtml ?></div> -<?php } ?> +<?php endif; ?> <?= $block->getExtendedElement($switchAttributeCode)->toHtml() ?> @@ -43,13 +43,13 @@ $isElementReadonly = $block->getElement() } else { if ($attribute) { <?php if ($attributeCode === 'price' && !$block->getCanEditPrice() && $block->getCanReadPrice() - && $block->getProduct()->isObjectNew()) { ?> + && $block->getProduct()->isObjectNew()): ?> <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> $attribute.value = <?= /* @escapeNotVerified */ $defaultProductPrice ?>; - <?php } else { ?> + <?php else: ?> $attribute.disabled = false; $attribute.addClassName('required-entry'); - <?php } ?> + <?php endif; ?> } if ($('dynamic-price-warning')) { $('dynamic-price-warning').hide(); @@ -58,9 +58,9 @@ $isElementReadonly = $block->getElement() } <?php if (!($attributeCode === 'price' && !$block->getCanEditPrice() - && !$block->getProduct()->isObjectNew())) { ?> + && !$block->getProduct()->isObjectNew())): ?> $('<?= /* @escapeNotVerified */ $switchAttributeCode ?>').observe('change', <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change); - <?php } ?> + <?php endif; ?> Event.observe(window, 'load', function(){ <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change(); }); diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index ff26d67bd8378..12da960a9c6cf 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -28,8 +28,17 @@ <?php endif; ?> <?php foreach ($items as $_item): ?> + <?php + $shipTogether = ($_item->getOrderItem()->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) ? + !$_item->getOrderItem()->isShipSeparately() : !$_item->getOrderItem()->getParentItem()->isShipSeparately() + ?> <?php $block->setPriceDataObject($_item) ?> <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php + if ($shipTogether) { + continue; + } + ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> @@ -60,14 +69,14 @@ </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <?= $block->getColumnHtml($_item, 'price') ?> <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <table class="qty-table"> <tr> <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> @@ -116,7 +125,7 @@ <?php endif; ?> </td> <td class="col-qty-invoice"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <?php if ($block->canEditQty()) : ?> <input type="text" class="input-text admin__control-text qty-input" diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js index e56cc6f32d804..49ee253ad1e88 100644 --- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js +++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js @@ -27,7 +27,8 @@ define([ '<% } %>', controlContainer: 'dd', // should be eliminated priceFormat: {}, - isFixedPrice: false + isFixedPrice: false, + optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]' }; $.widget('mage.priceBundle', { @@ -91,6 +92,8 @@ define([ if (changes) { priceBox.trigger('updatePrice', changes); } + + this._displayTierPriceBlock(bundleOption); this.updateProductSummary(); }, @@ -207,6 +210,35 @@ define([ return this; }, + /** + * Show or hide option tier prices block + * + * @param {Object} optionElement + * @private + */ + _displayTierPriceBlock: function (optionElement) { + var optionType = optionElement.prop('type'), + optionId, + optionValue, + optionTierPricesElements; + + if (optionType === 'select-one') { + optionId = utils.findOptionId(optionElement[0]); + optionValue = optionElement.val() || null; + optionTierPricesElements = $(this.options.optionTierPricesBlocksSelector.replace('{1}', optionId)); + + _.each(optionTierPricesElements, function (tierPriceElement) { + var selectionId = $(tierPriceElement).data('selection-id') + ''; + + if (selectionId === optionValue) { + $(tierPriceElement).show(); + } else { + $(tierPriceElement).hide(); + } + }); + } + }, + /** * Handler to update productSummary box */ @@ -374,8 +406,17 @@ define([ function applyTierPrice(oneItemPrice, qty, optionConfig) { var tiers = optionConfig.tierPrice, magicKey = _.keys(oneItemPrice)[0], + tiersFirstKey = _.keys(optionConfig)[0], lowest = false; + if (!tiers) {//tiers is undefined when options has only one option + tiers = optionConfig[tiersFirstKey].tierPrice; + } + + tiers.sort(function (a, b) {//sorting based on "price_qty" + return a['price_qty'] - b['price_qty']; + }); + _.each(tiers, function (tier, index) { if (tier['price_qty'] > qty) { return; diff --git a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml index 5b8c050e5af54..d12f2e8f6a952 100644 --- a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml +++ b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml @@ -29,10 +29,22 @@ <container name="product.info.bundle.options.top" as="product_info_bundle_options_top"> <block class="Magento\Catalog\Block\Product\View" name="bundle.back.button" as="backButton" before="-" template="Magento_Bundle::catalog/product/view/backbutton.phtml"/> </container> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi" name="product.info.bundle.options.multi" as="multi"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> </block> </referenceBlock> <referenceBlock name="product.info.form.options"> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index bda649eb603e6..830d03c826f32 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -19,6 +19,7 @@ <div class="nested options-list"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" @@ -38,6 +39,8 @@ <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml index 7ea89e8609818..1f33d97227ea3 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml @@ -21,6 +21,7 @@ <div class="nested options-list"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" name="bundle_option[<?= (int)$_option->getId() ?>]" @@ -57,6 +58,8 @@ <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> <span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml index 977daa2b2a446..4ea00f62b2043 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml @@ -20,6 +20,7 @@ <div class="control"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" @@ -39,6 +40,15 @@ </option> <?php endforeach; ?> </select> + <div id="option-tier-prices-<?= /* @escapeNotVerified */ $_option->getId() ?>" class="option-tier-prices"> + <?php foreach ($_selections as $_selection): ?> + <div data-role="selection-tier-prices" + data-selection-id="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" + class="selection-tier-prices"> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + </div> + <?php endforeach; ?> + </div> <?php endif; ?> <div class="nested"> <div class="field qty qty-holder"> diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml index 063d66edb9e70..74e1c5f874954 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml @@ -7,95 +7,111 @@ // @codingStandardsIgnoreFile /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ +$parentItem = $block->getItem(); +$items = array_merge([$parentItem], $parentItem->getChildrenItems()); +$index = 0; +$prevOptionId = ''; ?> -<?php $parentItem = $block->getItem() ?> -<?php $items = array_merge([$parentItem], $parentItem->getChildrenItems()); ?> -<?php $_index = 0 ?> -<?php $_prevOptionId = '' ?> +<?php foreach ($items as $item): ?> -<?php foreach ($items as $_item): ?> - - <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> - <?php $_showlastRow = true ?> + <?php if ($block->getItemOptions() + || $parentItem->getDescription() + || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) + && $parentItem->getGiftMessageId()): ?> + <?php $showLastRow = true; ?> <?php else: ?> - <?php $_showlastRow = false ?> + <?php $showLastRow = false; ?> <?php endif; ?> - <?php if ($_item->getParentItem()): ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($item->getParentItem()): ?> + <?php $attributes = $block->getSelectionAttributes($item) ?> + <?php if ($prevOptionId != $attributes['option_id']): ?> <tr class="options-label"> - <td class="col label" colspan="5"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></td> + <td class="col label" colspan="5"><?= $block->escapeHtml($attributes['option_label']); ?></td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> - <?php if (!$_item->getParentItem()): ?> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> - <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> +<tr id="order-item-row-<?= /* @noEscape */ $item->getId() ?>" + class="<?php if ($item->getParentItem()): ?> + item-options-container + <?php else: ?> + item-parent + <?php endif; ?>" + <?php if ($item->getParentItem()): ?> + data-th="<?= $block->escapeHtml($attributes['option_label']); ?>" + <?php endif; ?>> + <?php if (!$item->getParentItem()): ?> + <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')); ?>"> + <strong class="product name product-item-name"><?= $block->escapeHtml($item->getName()); ?></strong> </td> <?php else: ?> - <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> + <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')); ?>"> + <?= $block->getValueHtml($item); ?> + </td> <?php endif; ?> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($_item->getSku()) ?></td> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?php if (!$_item->getParentItem()): ?> - <?= $block->getItemPriceHtml() ?> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')); ?>"> + <?= /* @noEscape */ $block->prepareSku($item->getSku()); ?> + </td> + <td class="col price" data-th="<?= $block->escapeHtml(__('Price')); ?>"> + <?php if (!$item->getParentItem()): ?> + <?= /* @noEscape */ $block->getItemPriceHtml(); ?> <?php else: ?>   <?php endif; ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')) ?>"> + <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')); ?>"> <?php if ( - ($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + ($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())): ?> <ul class="items-qty"> <?php endif; ?> - <?php if (($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated())): ?> - <?php if ($_item->getQtyOrdered() > 0): ?> + <?php if (($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated())): ?> + <?php if ($item->getQtyOrdered() > 0): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Ordered') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Ordered')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyOrdered() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> + <?php if ($item->getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyCanceled() > 0): ?> + <?php if ($item->getQtyCanceled() > 0): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Canceled') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Canceled')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyCanceled() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyRefunded() > 0): ?> + <?php if ($item->getQtyRefunded() > 0): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Refunded') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Refunded')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyRefunded() * 1; ?></span> </li> <?php endif; ?> - <?php elseif ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately()): ?> + <?php elseif ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately()): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> </li> <?php else: ?> -   + <span class="content"><?= /* @noEscape */ $parentItem->getQtyOrdered() * 1; ?></span> <?php endif; ?> <?php if ( - ($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + ($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())):?> </ul> <?php endif; ?> </td> <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if (!$_item->getParentItem()): ?> - <?= $block->getItemRowTotalHtml() ?> + <?php if (!$item->getParentItem()): ?> + <?= /* @noEscape */ $block->getItemRowTotalHtml(); ?> <?php else: ?>   <?php endif; ?> @@ -103,33 +119,38 @@ </tr> <?php endforeach; ?> -<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +<?php if ($showLastRow && (($options = $block->getItemOptions()) || $block->escapeHtml($item->getDescription()))): ?> <tr> <td class="col options" colspan="5"> - <?php if ($_options = $block->getItemOptions()): ?> + <?php if ($options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <?php foreach ($options as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> + <?php $formattedOptionValue = $block->getFormatedOptionValue($option) ?> + <dd<?php if (isset($formattedOptionValue['full_view'])): ?> + class="tooltip wrapper" + <?php endif; ?>> + <?= /* @noEscape */ $formattedOptionValue['value'] ?> + <?php if (isset($formattedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> + <dt><?= $block->escapeHtml($option['label']); ?></dt> + <dd><?= /* @noEscape */ $formattedOptionValue['full_view']; ?></dd> </dl> </div> <?php endif; ?> </dd> <?php else: ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <dd><?= $block->escapeHtml((isset($option['print_value']) ? + $option['print_value'] : + $option['value'])); ?> + </dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> + <?= $block->escapeHtml($item->getDescription()); ?> </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php b/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php index b904d3f62a748..211d625fbc754 100644 --- a/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php +++ b/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php @@ -8,19 +8,22 @@ namespace Magento\BundleGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Bundle\Model\Product\Type as Type; /** - * {@inheritdoc} + * @inheritdoc */ class BundleProductTypeResolver implements TypeResolverInterface { + const BUNDLE_PRODUCT = 'BundleProduct'; + /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { - if (isset($data['type_id']) && $data['type_id'] == 'bundle') { - return 'BundleProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { + return self::BUNDLE_PRODUCT; } return ''; } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php index 149155c86275a..7608d6e9e4d97 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php @@ -61,6 +61,7 @@ public function __construct( * Add parent id/sku pair to use for option filter at fetch time. * * @param int $parentId + * @param int $parentEntityId * @param string $sku */ public function addParentFilterData(int $parentId, int $parentEntityId, string $sku) : void diff --git a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php index 727e18280d76f..b2aa0d000e9cf 100644 --- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php +++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php @@ -94,7 +94,7 @@ private function splitTags($tagsPattern) $formattedTags = explode('|', $tagsPattern); foreach ($formattedTags as $formattedTag) { if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) { - yield implode('|', array_unique($formattedTagsChunk)); + yield implode('|', $formattedTagsChunk); $formattedTagsChunk = []; $tagsBatchSize = 0; } @@ -103,7 +103,7 @@ private function splitTags($tagsPattern) $formattedTagsChunk[] = $formattedTag; } if (!empty($formattedTagsChunk)) { - yield implode('|', array_unique($formattedTagsChunk)); + yield implode('|', $formattedTagsChunk); } } @@ -118,6 +118,7 @@ private function splitTags($tagsPattern) private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk) { $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk]; + $unresponsiveServerError = []; foreach ($servers as $server) { $headers['Host'] = $server->getHost(); try { @@ -131,10 +132,30 @@ private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedT $socketAdapter->read(); $socketAdapter->close(); } catch (\Exception $e) { - $this->logger->critical($e->getMessage(), compact('server', 'formattedTagsChunk')); + $unresponsiveServerError[] = "Cache host: " . $server->getHost() . ":" . $server->getPort() . + "resulted in error message: " . $e->getMessage(); + } + } + + $errorCount = count($unresponsiveServerError); + + if ($errorCount > 0) { + $loggerMessage = implode(" ", $unresponsiveServerError); + + if ($errorCount == count($servers)) { + $this->logger->critical( + 'No cache server(s) could be purged ' . $loggerMessage, + compact('server', 'formattedTagsChunk') + ); return false; } + + $this->logger->warning( + 'Unresponsive cache server(s) hit' . $loggerMessage, + compact('server', 'formattedTagsChunk') + ); } + $this->logger->execute(compact('servers', 'formattedTagsChunk')); return true; } diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php index bdc8dfa218972..dd4974c5d842c 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Captcha\Observer; use Magento\Customer\Model\AuthenticationInterface; @@ -11,7 +12,10 @@ use Magento\Customer\Api\CustomerRepositoryInterface; /** + * Check captcha on user login page observer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class CheckUserLoginObserver implements ObserverInterface { @@ -140,7 +144,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $customer = $this->getCustomerRepository()->get($login); $this->getAuthentication()->processAuthenticationFailure($customer->getId()); } catch (NoSuchEntityException $e) { - //do nothing as customer existance is validated later in authenticate method + //do nothing as customer existence is validated later in authenticate method } $this->messageManager->addError(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml new file mode 100644 index 0000000000000..07329e2659876 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.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="AdminLoginWithCaptchaActionGroup" extends="LoginAsAdmin"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillPassword" userInput="{{captcha}}" selector="{{AdminLoginFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml new file mode 100644 index 0000000000000..a371f177e3552 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.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="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup"> + <waitForPageLoad stepKey="waitForPageLoaded" /> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="dontSeeCaptchaField"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="dontSeeCaptchaImage"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="dontSeeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="dontSeeCaptchaFieldAfterPageReload"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="dontSeeCaptchaImageAfterPageReload"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="dontSeeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml new file mode 100644 index 0000000000000..aa02588000d2b --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.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="AssertCaptchaVisibleOnAdminLoginFormActionGroup"> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaField}}" stepKey="seeCaptchaField"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaField}}" stepKey="seeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaImg}}" stepKey="seeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaReload}}" stepKey="seeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml new file mode 100644 index 0000000000000..d800c65cabb60 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.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="AssertCaptchaVisibleOnContactUsFormActionGroup"> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaField}}" stepKey="waitToSeeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaField}}" stepKey="waitToSeeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml new file mode 100644 index 0000000000000..6c09d1d49381f --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.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="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup"> + <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaField}}" stepKey="waitForCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaImg}}" stepKey="waitForCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaReload}}" stepKey="waitForCaptchaReloadButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml new file mode 100644 index 0000000000000..c68ffbfb5be4b --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.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="AssertCaptchaVisibleOnCustomerAccountInfoActionGroup"> + <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckbox" /> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" stepKey="seeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaImg}}" stepKey="seeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckboxAfterPageReload" /> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" stepKey="seeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaImg}}" stepKey="seeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaReload}}" stepKey="seeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml new file mode 100644 index 0000000000000..5616b099c026d --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.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="AssertCaptchaVisibleOnCustomerLoginFormActionGroup"> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="waitToSeeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="waitToSeeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml index beb2c2bffa135..f3b6eb1d9af84 100644 --- a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CaptchaFormsDisplayingActionGroup"> <click selector="{{CaptchaFormsDisplayingSection.store}}" stepKey="ClickToGoStores"/> <waitForPageLoad stepKey="waitForStoresLoaded"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml new file mode 100644 index 0000000000000..8aff3d5482f2c --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.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="StorefrontCustomerChangeEmailWithCaptchaActionGroup" extends="StorefrontCustomerChangeEmailActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + + <fillField selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" userInput="{{captcha}}" stepKey="fillCaptchaField" after="fillCurrentPassword" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml new file mode 100644 index 0000000000000..3546fa2e57a33 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.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="StorefrontFillContactUsFormWithCaptchaActionGroup" extends="StorefrontFillContactUsFormActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillComment" userInput="{{captcha}}" selector="{{StorefrontContactUsFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml new file mode 100644 index 0000000000000..d67ebc1a00768 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.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="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" extends="StorefrontFillCustomerAccountCreationFormActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillConfirmPassword" userInput="{{captcha}}" selector="{{StorefrontCustomerCreateFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml new file mode 100644 index 0000000000000..5ad727a8fe99d --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.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="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" extends="StorefrontFillCustomerLoginFormActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillPassword" userInput="{{captcha}}" selector="{{StorefrontCustomerSignInFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml new file mode 100644 index 0000000000000..90f48c320f2ac --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml @@ -0,0 +1,142 @@ +<?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="StorefrontCustomerCaptchaEnableConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontCustomerCaptchaDisableConfigData"> + <data key="path">customer/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerCreateFormConfigData"> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Create user</data> + <data key="value">user_create</data> + </entity> + <entity name="StorefrontCaptchaOnContactUsFormConfigData"> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Contact Us</data> + <data key="value">contact_us</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerLoginConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Login</data> + <data key="value">user_login</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerChangePasswordConfigData"> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Change password</data> + <data key="value">user_edit</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerForgotPasswordConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Forgot password</data> + <data key="value">user_forgotpassword</data> + </entity> + <entity name="StorefrontCustomerCaptchaModeAlwaysConfigData"> + <data key="path">customer/captcha/mode</data> + <data key="scope_id">0</data> + <data key="label">Always</data> + <data key="value">always</data> + </entity> + <entity name="StorefrontCustomerCaptchaModeAfterFailConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/mode</data> + <data key="scope_id">0</data> + <data key="label">After number of attempts to login</data> + <data key="value">after_fail</data> + </entity> + <entity name="StorefrontCustomerCaptchaLength3ConfigData"> + <data key="path">customer/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">3</data> + <data key="value">3</data> + </entity> + <entity name="StorefrontCustomerCaptchaSymbols1ConfigData"> + <data key="path">customer/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">1</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontCustomerCaptchaDefaultLengthConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">4-5</data> + <data key="value">4-5</data> + </entity> + <entity name="StorefrontCustomerCaptchaDefaultSymbolsConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + <data key="value">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + </entity> + <entity name="AdminCaptchaEnableConfigData"> + <!-- Magento default value --> + <data key="path">admin/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="AdminCaptchaDisableConfigData"> + <data key="path">admin/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="AdminCaptchaLength3ConfigData"> + <data key="path">admin/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">3</data> + <data key="value">3</data> + </entity> + <entity name="AdminCaptchaSymbols1ConfigData"> + <data key="path">admin/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">1</data> + <data key="value">1</data> + </entity> + <entity name="AdminCaptchaDefaultLengthConfigData"> + <!-- Magento default value --> + <data key="path">admin/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">4-5</data> + <data key="value">4-5</data> + </entity> + <entity name="AdminCaptchaDefaultSymbolsConfigData"> + <!-- Magento default value --> + <data key="path">admin/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + <data key="value">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + </entity> +</entities> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml new file mode 100644 index 0000000000000..d8fb206b8111c --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml @@ -0,0 +1,19 @@ +<?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="WrongCaptcha"> + <data key="value" unique="suffix">WrongCAPTCHA</data> + </entity> + + <!-- This CAPTCHA will only work if "StorefrontCustomerCaptchaLength3ConfigData" and "StorefrontCustomerCaptchaSymbols1ConfigData" config is set. --> + <entity name="PreconfiguredCaptcha"> + <data key="value">111</data> + </entity> +</entities> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml index 9db8110c0f64b..57a09219fe4db 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="CaptchaData"> <data key="createUser">Create user</data> <data key="login">Login</data> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml new file mode 100644 index 0000000000000..2bcc6fc542d82 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminLoginFormSection"> + <element name="captchaField" type="input" selector="#login-form input[name='captcha[backend_login]']" /> + <element name="captchaImg" type="block" selector="#login-form img#backend_login"/> + <element name="captchaReload" type="block" selector="#login-form img#captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml new file mode 100644 index 0000000000000..f587812576ff1 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsCaptchaSection"> + <element name="captchaField" type="input" selector="#captcha_contact_us"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml new file mode 100644 index 0000000000000..60cf961ba7e8c --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsFormSection"> + <element name="captchaField" type="input" selector="#contact-form input[name='captcha[contact_us]']" /> + <element name="captchaImg" type="block" selector="#contact-form img.captcha-img"/> + <element name="captchaReload" type="block" selector="#contact-form button.captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..a273c8d4abd28 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAccountInformationSection"> + <element name="captchaField" type="input" selector="#captcha_user_edit"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml new file mode 100644 index 0000000000000..f48e6124cb214 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerCreateFormSection"> + <element name="captchaField" type="input" selector="#captcha_user_create"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml index 7a0557c4a2744..54aa36d1ca267 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StorefrontCustomerSignInPopupFormSection"> + <section name="StorefrontCustomerSignInFormSection"> <element name="captchaField" type="input" selector="#captcha_user_login"/> <element name="captchaImg" type="block" selector=".captcha-img"/> <element name="captchaReload" type="block" selector=".captcha-reload"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml new file mode 100644 index 0000000000000..7a0557c4a2744 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerSignInPopupFormSection"> + <element name="captchaField" type="input" selector="#captcha_user_login"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml new file mode 100644 index 0000000000000..e5ee55910df65 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml @@ -0,0 +1,68 @@ +<?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="AdminLoginWithCaptchaTest"> + <annotations> + <features value="Captcha"/> + <stories value="Admin login + Captcha"/> + <title value="Captcha on Admin login form"/> + <description value="Test creation for admin login with captcha."/> + <testCaseId value="MC-14012" /> + <severity value="MAJOR"/> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI command="config:set {{AdminCaptchaLength3ConfigData.path}} {{AdminCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{AdminCaptchaSymbols1ConfigData.path}} {{AdminCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </before> + <after> + <magentoCLI command="config:set {{AdminCaptchaDefaultLengthConfigData.path}} {{AdminCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{AdminCaptchaDefaultSymbolsConfigData.path}} {{AdminCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsFirstAttempt"> + <argument name="adminUser" value="AdminUserWrongCredentials" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeFirstLoginErrorMessage" /> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsSecondAttempt"> + <argument name="adminUser" value="AdminUserWrongCredentials" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeSecondLoginErrorMessage" /> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsThirdAttempt"> + <argument name="adminUser" value="AdminUserWrongCredentials" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeThirdLoginErrorMessage" /> + + <!-- Check captcha visibility on admin login page --> + <actionGroup ref="AssertCaptchaVisibleOnAdminLoginFormActionGroup" stepKey="assertCaptchaVisible" /> + + <!-- Submit form with incorrect captcha --> + <actionGroup ref="AdminLoginWithCaptchaActionGroup" stepKey="loginAsAdminWithIncorrectCaptcha"> + <argument name="adminUser" value="DefaultAdminUser" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeIncorrectCaptchaErrorMessage"> + <argument name="message" value="Incorrect CAPTCHA." /> + </actionGroup> + <actionGroup ref="AssertCaptchaVisibleOnAdminLoginFormActionGroup" stepKey="assertCaptchaVisibleAfterIncorrectCaptcha" /> + + <actionGroup ref="AdminLoginWithCaptchaActionGroup" stepKey="loginAsAdminWithCorrectCaptcha"> + <argument name="adminUser" value="DefaultAdminUser" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="verifyAdminLoggedIn" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml new file mode 100644 index 0000000000000..8f9c5828e2f5e --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml @@ -0,0 +1,19 @@ +<?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="AdminResetUserPasswordFailedTest"> + <before> + <magentoCLI command="config:set {{AdminCaptchaDisableConfigData.path}} {{AdminCaptchaDisableConfigData.value}} " stepKey="disableAdminCaptcha"/> + </before> + <after> + <magentoCLI command="config:set {{AdminCaptchaEnableConfigData.path}} {{AdminCaptchaEnableConfigData.value}} " stepKey="enableAdminCaptcha"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml index a088266f760a5..035e58de06ccf 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="CaptchaFormsDisplayingTest"> <annotations> <features value="Captcha"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml new file mode 100644 index 0000000000000..54237087227d8 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml @@ -0,0 +1,102 @@ +<?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="StorefrontCaptchaEditCustomerEmailTest"> + <annotations> + <features value="Captcha"/> + <stories value="Customer Account Info Edit + Captcha"/> + <title value="Test for checking captcha on the customer account edit page."/> + <description value="Test for checking captcha on the customer account edit page and customer is locked."/> + <testCaseId value="MC-14013" /> + <severity value="MAJOR"/> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Setup CAPTCHA for testing --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerChangePasswordConfigData.path}} {{StorefrontCaptchaOnCustomerChangePasswordConfigData.value}}" stepKey="enableUserEditCaptcha"/> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <createData entity="Simple_US_Customer" stepKey="customer"/> + <!-- Sign in as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + </before> + <after> + <!-- Revert Captcha forms configurations --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + + <!-- Open Customer edit page --> + <actionGroup ref="StorefrontOpenCustomerAccountInfoEditPageActionGroup" stepKey="goToCustomerEditPage" /> + + <!-- Update email with incorrect password 3 times. --> + <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailFirstAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageFirstAttempt"> + <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailSecondAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageSecondAttempt"> + <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailThirdAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageThirdAttempt"> + <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <!-- Check captcha visibility after incorrect password submit form --> + <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountInfoActionGroup" stepKey="assertCaptchaVisible" /> + + <!-- Try to submit form with incorrect captcha --> + <actionGroup ref="StorefrontCustomerChangeEmailWithCaptchaActionGroup" stepKey="changeEmailWithIncorrectCaptcha"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageAfterIncorrectCaptcha"> + <argument name="message" value="Incorrect CAPTCHA" /> + <argument name="messageType" value="error" /> + </actionGroup> + + <!-- Update customer email correct password and CAPTCHA --> + <actionGroup ref="StorefrontCustomerChangeEmailWithCaptchaActionGroup" stepKey="changeEmailCorrectAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="$$customer.password$$" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageCorrectAttempt" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml new file mode 100644 index 0000000000000..0c6a3f31c1df2 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml @@ -0,0 +1,64 @@ +<?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="StorefrontCaptchaOnContactUsTest"> + <annotations> + <features value="Captcha"/> + <stories value="Submit Contact us form + Captcha"/> + <title value="Captcha on contact us form test"/> + <description value="Test creation for send comment using the contact us form with captcha."/> + <testCaseId value="MC-14103" /> + <severity value="MAJOR"/> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="config:set {{StorefrontCaptchaOnContactUsFormConfigData.path}} {{StorefrontCaptchaOnContactUsFormConfigData.value}}" stepKey="enableUserEditCaptcha"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </after> + + <!-- Open storefront contact us form --> + <actionGroup ref="StorefrontOpenContactUsPageActionGroup" stepKey="goToContactUsPage" /> + + <!-- Check Captcha items --> + <actionGroup ref="AssertCaptchaVisibleOnContactUsFormActionGroup" stepKey="seeCaptchaOnContactUsForm" /> + + <!-- Submit Contact Us form --> + <actionGroup ref="StorefrontFillContactUsFormWithCaptchaActionGroup" stepKey="fillContactUsFormWithWrongCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="contactUsData" value="DefaultContactUsData" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontSubmitContactUsFormActionGroup" stepKey="submitContactUsFormWithWrongCaptcha" /> + + <!-- Check Captcha items after form reload --> + <actionGroup ref="AssertMessageContactUsFormActionGroup" stepKey="verifyErrorMessage"> + <argument name="message" value="Incorrect CAPTCHA" /> + <argument name="messageType" value="error" /> + </actionGroup> + <actionGroup ref="AssertCaptchaVisibleOnContactUsFormActionGroup" stepKey="seeCaptchaOnContactUsFormAfterWrongCaptcha" /> + + <actionGroup ref="StorefrontFillContactUsFormWithCaptchaActionGroup" stepKey="fillContactUsFormWithCorrectCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="contactUsData" value="DefaultContactUsData" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontSubmitContactUsFormActionGroup" stepKey="submitContactUsFormWithCorrectCaptcha" /> + <actionGroup ref="AssertMessageContactUsFormActionGroup" stepKey="verifySuccessMessage" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml new file mode 100644 index 0000000000000..5a1be68d3f251 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml @@ -0,0 +1,79 @@ +<?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="StorefrontCaptchaOnCustomerLoginTest"> + <annotations> + <features value="Captcha"/> + <stories value="Login with Customer Account + Captcha"/> + <title value="Captcha customer login page test"/> + <description value="Check CAPTCHA on Storefront Login Page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14010" /> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <createData entity="Simple_US_Customer" stepKey="customer"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + + <!-- Open storefront login form --> + <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage" /> + + <!-- Login with wrong credentials 3 times --> + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormFirstAttempt"> + <argument name="customer" value="Colorado_US_Customer" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFirstAttempt" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterFirstAttempt" /> + <actionGroup ref="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup" stepKey="dontSeeCaptchaAfterFirstAttempt" /> + + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormSecondAttempt"> + <argument name="customer" value="Colorado_US_Customer" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonSecondAttempt" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterSecondAttempt" /> + <actionGroup ref="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup" stepKey="dontSeeCaptchaAfterSecondAttempt" /> + + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormThirdAttempt"> + <argument name="customer" value="Colorado_US_Customer" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonThirdAttempt" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterThirdAttempt" /> + <actionGroup ref="AssertCaptchaVisibleOnCustomerLoginFormActionGroup" stepKey="seeCaptchaAfterThirdAttempt" /> + + <!-- Submit form with incorrect captcha --> + <actionGroup ref="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" stepKey="fillLoginFormCorrectAccountIncorrectCaptcha"> + <argument name="customer" value="$$customer$$" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonCorrectAccountIncorrectCaptcha" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterIncorrectCaptcha"> + <argument name="message" value="Incorrect CAPTCHA" /> + </actionGroup> + + <actionGroup ref="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" stepKey="fillLoginFormCorrectAccountCorrectCaptcha"> + <argument name="customer" value="$$customer$$" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonCorrectAccountCorrectCaptcha" /> + <actionGroup ref="AssertCustomerWelcomeMessageActionGroup" stepKey="assertCustomerLoggedIn"> + <argument name="customerFullName" value="$$customer.firstname$$ $$customer.lastname$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml new file mode 100644 index 0000000000000..2c331f958e467 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml @@ -0,0 +1,68 @@ +<?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="StorefrontCaptchaRegisterNewCustomerTest"> + <annotations> + <features value="Captcha"/> + <stories value="Create New Customer Account + Captcha"/> + <title value="Test creation for customer register with captcha on storefront."/> + <description value="Test creation for customer register with captcha on storefront."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14805" /> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Enable captcha for customer. --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerCreateFormConfigData.path}} {{StorefrontCaptchaOnCustomerCreateFormConfigData.value}}" stepKey="enableUserRegistrationCaptcha" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAlwaysConfigData.path}} {{StorefrontCustomerCaptchaModeAlwaysConfigData.value}}" stepKey="alwaysEnableCaptcha" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </before> + <after> + <!-- Set default configuration. --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAfterFailConfigData.path}} {{StorefrontCustomerCaptchaModeAfterFailConfigData.value}}" stepKey="defaultCaptchaMode" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </after> + + <!-- Open Customer registration page --> + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="goToCustomerAccountCreatePage" /> + + <!-- Check captcha visibility registration page load --> + <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup" stepKey="verifyCaptchaVisible" /> + + <!-- Submit form with incorrect captcha --> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" stepKey="fillNewCustomerAccountFormWithIncorrectCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="clickCreateAnAccountButton" /> + + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertMessage"> + <argument name="message" value="Incorrect CAPTCHA" /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup" stepKey="verifyCaptchaVisibleAfterFail" /> + + <!-- Submit form with correct captcha --> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" stepKey="fillNewCustomerAccountFormWithCorrectCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="clickCreateAnAccountButtonAfterCorrectCaptcha" /> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertSuccessMessage" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml new file mode 100644 index 0000000000000..36d7989b9acc1 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml @@ -0,0 +1,19 @@ +<?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="StorefrontResetCustomerPasswordFailedTest"> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php b/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php index 34656d522a72c..a65355c690923 100644 --- a/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php +++ b/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php @@ -39,7 +39,7 @@ public function delete(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $p /** * Remove the product assignment from the category by category id and sku * - * @param string $categoryId + * @param int $categoryId * @param string $sku * @return bool will returned True if products successfully deleted * diff --git a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php index b9a23e9d08ec3..1940a0ac80c0b 100644 --- a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php @@ -1,7 +1,5 @@ <?php /** - * Category data interface - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,6 +7,8 @@ namespace Magento\Catalog\Api\Data; /** + * Category data interface. + * * @api * @since 100.0.2 */ @@ -46,11 +46,15 @@ interface CategoryInterface extends \Magento\Framework\Api\CustomAttributesDataI /**#@-*/ /** + * Retrieve category id. + * * @return int|null */ public function getId(); /** + * Set category id. + * * @param int $id * @return $this */ @@ -74,7 +78,7 @@ public function setParentId($parentId); /** * Get category name * - * @return string + * @return string|null */ public function getName(); @@ -132,60 +136,82 @@ public function getLevel(); public function setLevel($level); /** + * Retrieve children ids comma separated. + * * @return string|null */ public function getChildren(); /** + * Retrieve category creation date and time. + * * @return string|null */ public function getCreatedAt(); /** + * Set category creation date and time. + * * @param string $createdAt * @return $this */ public function setCreatedAt($createdAt); /** + * Retrieve category last update date and time. + * * @return string|null */ public function getUpdatedAt(); /** + * Set category last update date and time. + * * @param string $updatedAt * @return $this */ public function setUpdatedAt($updatedAt); /** + * Retrieve category full path. + * * @return string|null */ public function getPath(); /** + * Set category full path. + * * @param string $path * @return $this */ public function setPath($path); /** + * Retrieve available sort by for category. + * * @return string[]|null */ public function getAvailableSortBy(); /** + * Set available sort by for category. + * * @param string[]|string $availableSortBy * @return $this */ public function setAvailableSortBy($availableSortBy); /** + * Get category is included in menu. + * * @return bool|null */ public function getIncludeInMenu(); /** + * Set category is included in menu. + * * @param bool $includeInMenu * @return $this */ diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php index 4cdb2631edea5..45b070d2706dc 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php @@ -92,6 +92,7 @@ public function setWidth($width); /** * Retrieve image label + * * Image label is short description of this image * * @return string @@ -111,7 +112,7 @@ public function setLabel($label); /** * Retrieve resize width * - * This width is image dimension, which represents the width, that can be used for perfomance improvements + * This width is image dimension, which represents the width, that can be used for performance improvements * * @return float * @since 101.1.0 @@ -128,6 +129,8 @@ public function getResizedWidth(); public function setResizedWidth($width); /** + * Set resized height + * * @param string $height * @return void * @since 101.1.0 diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php index 2ef4d068317dd..9768b3c08c8ab 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php @@ -8,6 +8,7 @@ /** * Price interface. + * * @api * @since 101.1.0 */ @@ -23,6 +24,7 @@ public function getFinalPrice(); /** * Set the final price: usually it calculated as minimal price of the product + * * Can be different depends on type of product * * @param float $finalPrice @@ -33,6 +35,7 @@ public function setFinalPrice($finalPrice); /** * Retrieve max price of a product + * * E.g. for product with custom options is price with the most expensive custom option * * @return float @@ -51,6 +54,7 @@ public function setMaxPrice($maxPrice); /** * Set max regular price + * * Max regular price is the same, as maximum price, except of excluding calculating special price and catalog rules * in it * @@ -105,6 +109,8 @@ public function setSpecialPrice($specialPrice); public function getSpecialPrice(); /** + * Retrieve minimal price + * * @return float * @since 101.1.0 */ @@ -129,6 +135,7 @@ public function getRegularPrice(); /** * Regular price - is price of product without discounts and special price with taxes and fixed product tax + * * Usually this price is corresponding to price in admin panel of product * * @param float $regularPrice @@ -148,7 +155,7 @@ public function getFormattedPrices(); /** * Set dto with formatted prices * - * @param string[] $formattedPriceInfo + * @param FormattedPriceInfoInterface $formattedPriceInfo * @return void * @since 101.1.0 */ diff --git a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php index 910168d8854e7..166a1aba76b61 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php @@ -30,7 +30,7 @@ public function getAddToCartButton(); /** * Set information needed for render "Add To Cart" button on front * - * @param \Magento\Catalog\Api\Data\ProductRender\ButtonInterface $addToCartData + * @param ButtonInterface $cartAddToCartButton * @return void * @since 101.1.0 */ @@ -47,7 +47,7 @@ public function getAddToCompareButton(); /** * Set information needed for render "Add To Compare" button on front * - * @param ButtonInterface $compareUrlData + * @param ButtonInterface $compareButton * @return string * @since 101.1.0 */ @@ -55,6 +55,7 @@ public function setAddToCompareButton(ButtonInterface $compareButton); /** * Provide information needed for render prices and adjustments for different product types on front + * * Prices are represented in raw format and in current currency * * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface @@ -73,6 +74,7 @@ public function setPriceInfo(PriceInfoInterface $priceInfo); /** * Provide enough information, that needed to render image on front + * * Images can be separated by image codes * * @return \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] @@ -167,6 +169,7 @@ public function getIsSalable(); /** * Set information about product saleability (Stock, other conditions) + * * Is used to provide information to frontend JS renders * You can add plugin, in order to hide product on product page or product list on front * @@ -178,6 +181,7 @@ public function setIsSalable($isSalable); /** * Provide information about current store id or requested store id + * * Product should be assigned to provided store id * This setting affect store scope attributes * @@ -197,6 +201,7 @@ public function setStoreId($storeId); /** * Provide current or desired currency code to product + * * This setting affect formatted prices* * * @return string diff --git a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php index f79efa4c814d7..954acd35a07db 100644 --- a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php +++ b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php @@ -4,18 +4,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Api; /** - * Interface which provides product renders information for products + * Interface which provides product renders information for products. + * * @api * @since 101.1.0 */ interface ProductRenderListInterface { /** - * Collect and retrieve the list of product render info - * This info contains raw prices and formated prices, product name, stock status, store_id, etc + * Collect and retrieve the list of product render info. + * + * This info contains raw prices and formatted prices, product name, stock status, store_id, etc. + * * @see \Magento\Catalog\Api\Data\ProductRenderInfoDtoInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php index 331679874629b..ffb648cdf438a 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php @@ -67,6 +67,8 @@ public function getCategory() } /** + * Get category id + * * @return int|string|null */ public function getCategoryId() @@ -78,6 +80,8 @@ public function getCategoryId() } /** + * Get category name + * * @return string */ public function getCategoryName() @@ -86,6 +90,8 @@ public function getCategoryName() } /** + * Get category path + * * @return mixed */ public function getCategoryPath() @@ -97,6 +103,8 @@ public function getCategoryPath() } /** + * Check store root category + * * @return bool */ public function hasStoreRootCategory() @@ -109,6 +117,8 @@ public function hasStoreRootCategory() } /** + * Get store from request + * * @return Store */ public function getStore() @@ -118,6 +128,8 @@ public function getStore() } /** + * Get root category for tree + * * @param mixed|null $parentNodeCategory * @param int $recursionLevel * @return Node|array|null @@ -149,10 +161,11 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) $root = $tree->getNodeById($rootId); - if ($root && $rootId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + if ($root) { $root->setIsVisible(true); - } elseif ($root && $root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { - $root->setName(__('Root')); + if ($root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + $root->setName(__('Root')); + } } $this->_coreRegistry->register('root', $root); @@ -162,6 +175,8 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) } /** + * Get Default Store Id + * * @return int */ protected function _getDefaultStoreId() @@ -170,6 +185,8 @@ protected function _getDefaultStoreId() } /** + * Get category collection + * * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getCategoryCollection() @@ -227,6 +244,8 @@ public function getRootByIds($ids) } /** + * Get category node for tree + * * @param mixed $parentNodeCategory * @param int $recursionLevel * @return Node @@ -249,6 +268,8 @@ public function getNode($parentNodeCategory, $recursionLevel = 2) } /** + * Get category save url + * * @param array $args * @return string */ @@ -260,6 +281,8 @@ public function getSaveUrl(array $args = []) } /** + * Get category edit url + * * @return string */ public function getEditUrl() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php index b77a5e2e95241..3266922d116ec 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper; +/** + * Pricestep Helper + */ class Pricestep extends \Magento\Framework\Data\Form\Element\Text { /** @@ -40,7 +43,7 @@ public function getElementHtml() $disabled = true; } - parent::addClass('validate-number validate-number-range number-range-0.01-1000000000'); + parent::addClass('validate-number validate-number-range number-range-0.01-9999999999999999'); $html = parent::getElementHtml(); $htmlId = 'use_config_' . $this->getHtmlId(); $html .= '<br/><input id="' . $htmlId . '" name="use_config[]" value="' . $this->getId() . '"'; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index a67f55235b6df..83ec501592489 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -71,7 +71,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -80,7 +80,7 @@ protected function _construct() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { @@ -182,6 +182,8 @@ public function getSuggestedCategoriesJson($namePart) } /** + * Get add root button html + * * @return string */ public function getAddRootButtonHtml() @@ -190,6 +192,8 @@ public function getAddRootButtonHtml() } /** + * Get add sub button html + * * @return string */ public function getAddSubButtonHtml() @@ -198,6 +202,8 @@ public function getAddSubButtonHtml() } /** + * Get expand button html + * * @return string */ public function getExpandButtonHtml() @@ -206,6 +212,8 @@ public function getExpandButtonHtml() } /** + * Get collapse button html + * * @return string */ public function getCollapseButtonHtml() @@ -214,6 +222,8 @@ public function getCollapseButtonHtml() } /** + * Get store switcher + * * @return string */ public function getStoreSwitcherHtml() @@ -222,6 +232,8 @@ public function getStoreSwitcherHtml() } /** + * Get loader tree url + * * @param bool|null $expanded * @return string */ @@ -235,6 +247,8 @@ public function getLoadTreeUrl($expanded = null) } /** + * Get nodes url + * * @return string */ public function getNodesUrl() @@ -243,6 +257,8 @@ public function getNodesUrl() } /** + * Get switcher tree url + * * @return string */ public function getSwitchTreeUrl() @@ -254,6 +270,8 @@ public function getSwitchTreeUrl() } /** + * Get is was expanded + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -263,6 +281,8 @@ public function getIsWasExpanded() } /** + * Get move url + * * @return string */ public function getMoveUrl() @@ -271,6 +291,8 @@ public function getMoveUrl() } /** + * Get tree + * * @param mixed|null $parenNodeCategory * @return array */ @@ -282,6 +304,8 @@ public function getTree($parenNodeCategory = null) } /** + * Get tree json + * * @param mixed|null $parenNodeCategory * @return string */ @@ -367,7 +391,7 @@ protected function _getNodeJson($node, $level = 0) } } - if ($isParent || $node->getLevel() < 2) { + if ($isParent || $node->getLevel() < 1) { $item['expanded'] = true; } @@ -390,6 +414,8 @@ public function buildNodeName($node) } /** + * Is category movable + * * @param Node|array $node * @return bool */ @@ -403,6 +429,8 @@ protected function _isCategoryMoveable($node) } /** + * Is parent selected category + * * @param Node|array $node * @return bool */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php index ad6df27b89334..8f1d1dcf7eedf 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ +namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset; + /** * Catalog fieldset element renderer * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset; - class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element { /** @@ -29,7 +29,7 @@ public function getDataObject() } /** - * Retireve associated with element attribute object + * Retrieve associated with element attribute object * * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php index dd09e40ac5b35..1b6756968662f 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php @@ -4,11 +4,6 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form main tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; use Magento\Backend\Block\Widget\Form\Generic; @@ -18,6 +13,8 @@ use Magento\Framework\App\ObjectManager; /** + * Product attribute add/edit form main tab + * * @api * @since 100.0.2 */ @@ -73,6 +70,7 @@ public function __construct( * Adding product form elements for editing attribute * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD) */ protected function _prepareForm() @@ -255,7 +253,7 @@ protected function _prepareForm() } /** - * Initialize form fileds values + * Initialize form fields values * * @return $this */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php index 4aa01b467d451..964872b6e51bd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php @@ -70,11 +70,11 @@ public function getFieldSuffix() * Retrieve current store id * * @return int + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreId() { - $storeId = $this->getRequest()->getParam('store'); - return (int) $storeId; + return (int)$this->getRequest()->getParam('store'); } /** @@ -99,6 +99,8 @@ public function getTabLabel() } /** + * Return Tab title. + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -107,7 +109,7 @@ public function getTabTitle() } /** - * @return bool + * @inheritdoc */ public function canShowTab() { @@ -115,7 +117,7 @@ public function canShowTab() } /** - * @return bool + * @inheritdoc */ public function isHidden() { @@ -123,6 +125,8 @@ public function isHidden() } /** + * Get availability status. + * * @param string $fieldName * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php index c8153df41430e..23b927598e8e7 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php @@ -322,7 +322,7 @@ protected function _prepareColumns() } /** - * Rerieve grid URL + * Retrieve grid URL * * @return string */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php index 04c3a208b97f9..37ad3f4bea20e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php @@ -8,7 +8,7 @@ use Magento\Backend\Block\Template\Context; use Magento\Backend\Block\Widget\Accordion; -use Magento\Backend\Block\Widget\Tabs as WigetTabs; +use Magento\Backend\Block\Widget\Tabs as WidgetTabs; use Magento\Backend\Model\Auth\Session; use Magento\Catalog\Helper\Catalog; use Magento\Catalog\Helper\Data; @@ -22,7 +22,7 @@ * Admin product edit tabs * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Tabs extends WigetTabs +class Tabs extends WidgetTabs { const BASIC_TAB_GROUP_CODE = 'basic'; @@ -109,7 +109,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -119,6 +119,8 @@ protected function _construct() } /** + * Get group collection. + * * @param int $attributeSetId * @return \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection */ @@ -131,10 +133,11 @@ public function getGroupCollection($attributeSetId) } /** - * @return $this + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _prepareLayout() { @@ -315,6 +318,8 @@ public function getAttributeTabBlock() } /** + * Set attribute tab block. + * * @param string $attributeTabBlock * @return $this */ @@ -337,6 +342,8 @@ protected function _translateHtml($html) } /** + * Get accordion. + * * @param string $parentTab * @return string */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php index 1f74969c3d169..7f80aece60ee0 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ +namespace Magento\Catalog\Block\Adminhtml\Product\Frontend\Product; + +use Magento\Framework\Data\Form\Element\AbstractElement; + /** * Fieldset config form element renderer * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Catalog\Block\Adminhtml\Product\Frontend\Product; - -use Magento\Framework\Data\Form\Element\AbstractElement; - class Watermark extends \Magento\Backend\Block\AbstractBlock implements \Magento\Framework\Data\Form\Element\Renderer\RendererInterface { @@ -60,6 +60,8 @@ public function __construct( } /** + * Render form element as HTML + * * @param AbstractElement $element * @return string */ @@ -124,13 +126,14 @@ public function render(AbstractElement $element) } /** + * Get header html for render + * * @param AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function _getHeaderHtml($element) { - $id = $element->getHtmlId(); $default = !$this->getRequest()->getParam('website') && !$this->getRequest()->getParam('store'); $html = '<h4 class="icon-head head-edit-form">' . $element->getLegend() . '</h4>'; @@ -148,6 +151,8 @@ protected function _getHeaderHtml($element) } /** + * Get footer html for render + * * @param AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php b/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php index ac1fd8c692ed2..c296a5aa0dbbd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php @@ -9,6 +9,7 @@ /** * Class NotifyStock + * * @package Magento\Catalog\Block\Adminhtml\Rss */ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataProviderInterface @@ -41,7 +42,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -50,12 +51,12 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRssData() { - $newUrl = $this->rssUrlBuilder->getUrl(['_secure' => true, '_nosecret' => true, 'type' => 'notifystock']); - $title = __('Low Stock Products'); + $newUrl = $this->rssUrlBuilder->getUrl(['_secure' => true, '_nosecret' => true, 'type' => 'notifystock']); + $title = __('Low Stock Products')->render(); $data = ['title' => $title, 'description' => $title, 'link' => $newUrl, 'charset' => 'UTF-8']; foreach ($this->rssModel->getProductsCollection() as $item) { @@ -65,7 +66,7 @@ public function getRssData() ['id' => $item->getId(), '_secure' => true, '_nosecret' => true] ); $qty = 1 * $item->getQty(); - $description = __('%1 has reached a quantity of %2.', $item->getName(), $qty); + $description = __('%1 has reached a quantity of %2.', $item->getName(), $qty)->render(); $data['entries'][] = ['title' => $item->getName(), 'link' => $url, 'description' => $description]; } @@ -73,7 +74,7 @@ public function getRssData() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCacheLifetime() { @@ -81,7 +82,7 @@ public function getCacheLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAllowed() { @@ -89,7 +90,7 @@ public function isAllowed() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFeeds() { @@ -97,7 +98,7 @@ public function getFeeds() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAuthRequired() { diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index c1b255c762dbb..c1d79894162ae 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -178,8 +178,9 @@ private function getDefaultListingMode() } /** - * Need use as _prepareLayout - but problem in declaring collection from - * another block (was problem with search result) + * Need use as _prepareLayout - but problem in declaring collection from another block. + * (was problem with search result) + * * @return $this */ protected function _beforeToHtml() @@ -188,7 +189,9 @@ protected function _beforeToHtml() $this->addToolbarBlock($collection); - $collection->load(); + if (!$collection->isLoaded()) { + $collection->load(); + } return parent::_beforeToHtml(); } @@ -262,6 +265,8 @@ public function getToolbarHtml() } /** + * Set collection. + * * @param AbstractCollection $collection * @return $this */ @@ -272,7 +277,9 @@ public function setCollection($collection) } /** - * @param array|string|integer| Element $code + * Add attribute. + * + * @param array|string|integer|Element $code * @return $this */ public function addAttribute($code) @@ -282,6 +289,8 @@ public function addAttribute($code) } /** + * Get price block template. + * * @return mixed */ public function getPriceBlockTemplate() @@ -371,6 +380,8 @@ public function getAddToCartPostParams(Product $product) } /** + * Get product price. + * * @param Product $product * @return string */ @@ -396,8 +407,8 @@ public function getProductPrice(Product $product) } /** - * Specifies that price rendering should be done for the list of products - * i.e. rendering happens in the scope of product list, but not single product + * Specifies that price rendering should be done for the list of products. + * (rendering happens in the scope of product list, but not single product) * * @return Render */ diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index cb59d86a74512..5b9777cbfd1e7 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -16,6 +16,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Attributes attributes block + * * @api * @since 100.0.2 */ @@ -56,6 +58,8 @@ public function __construct( } /** + * Returns a Product + * * @return Product */ public function getProduct() @@ -88,9 +92,9 @@ public function getAdditionalData(array $excludeAttr = []) $value = $this->priceCurrency->convertAndFormat($value); } - if (is_string($value) && strlen($value)) { + if (is_string($value) && strlen(trim($value))) { $data[$attribute->getAttributeCode()] = [ - 'label' => __($attribute->getStoreLabel()), + 'label' => $attribute->getStoreLabel(), 'value' => $value, 'code' => $attribute->getAttributeCode(), ]; diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php new file mode 100644 index 0000000000000..e76c5bf201334 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View; + +/** + * Product details block. + * + * Holds a group of blocks to show as tabs. + * + * @api + */ +class Details extends \Magento\Framework\View\Element\Template +{ + /** + * Get sorted child block names. + * + * @param string $groupName + * @param string $callback + * @throws \Magento\Framework\Exception\LocalizedException + * + * @return array + */ + public function getGroupSortedChildNames(string $groupName, string $callback): array + { + $groupChildNames = $this->getGroupChildNames($groupName, $callback); + $layout = $this->getLayout(); + + $childNamesSortOrder = []; + + foreach ($groupChildNames as $childName) { + $alias = $layout->getElementAlias($childName); + $sortOrder = (int)$this->getChildData($alias, 'sort_order') ?? 0; + + $childNamesSortOrder[$sortOrder] = $childName; + } + + ksort($childNamesSortOrder, SORT_NUMERIC); + + return $childNamesSortOrder; + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Gallery.php b/app/code/Magento/Catalog/Block/Product/View/Gallery.php index 706d9b83b9711..8b98fbdc8f7ef 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Gallery.php +++ b/app/code/Magento/Catalog/Block/Product/View/Gallery.php @@ -207,8 +207,8 @@ public function isMainImage($image) */ public function getImageAttribute($imageId, $attributeName, $default = null) { - $attributes = - $this->getConfigView()->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); + $attributes = $this->getConfigView() + ->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); return $attributes[$attributeName] ?? $default; } diff --git a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php new file mode 100644 index 0000000000000..0384c9cd9acce --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Block\Product\View; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Block\Product\Context; +use Magento\Framework\Stdlib\ArrayUtils; + +/** + * Gallery options block. + */ +class GalleryOptions extends AbstractView implements ArgumentInterface +{ + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Gallery + */ + private $gallery; + + /** + * @param Context $context + * @param ArrayUtils $arrayUtils + * @param Json $jsonSerializer + * @param Gallery $gallery + * @param array $data + */ + public function __construct( + Context $context, + ArrayUtils $arrayUtils, + Json $jsonSerializer, + Gallery $gallery, + array $data = [] + ) { + $this->gallery = $gallery; + $this->jsonSerializer = $jsonSerializer; + parent::__construct($context, $arrayUtils, $data); + } + + /** + * Retrieve gallery options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getOptionsJson() + { + $optionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/nav"))) { + $optionItems['nav'] = $this->getVar("gallery/nav") ? 'true' : 'false'; + } else { + $optionItems['nav'] = $this->escapeHtml($this->getVar("gallery/nav")); + } + + $optionItems['loop'] = $this->getVar("gallery/loop"); + $optionItems['keyboard'] = $this->getVar("gallery/keyboard"); + $optionItems['arrows'] = $this->getVar("gallery/arrows"); + $optionItems['allowfullscreen'] = $this->getVar("gallery/allowfullscreen"); + $optionItems['showCaption'] = $this->getVar("gallery/caption"); + $optionItems['width'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + $optionItems['thumbwidth'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + + if ($this->gallery->getImageAttribute('product_page_image_small', 'height') || + $this->gallery->getImageAttribute('product_page_image_small', 'width')) { + $optionItems['thumbheight'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + } + + if ($this->gallery->getImageAttribute('product_page_image_medium', 'height') || + $this->gallery->getImageAttribute('product_page_image_medium', 'width')) { + $optionItems['height'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + } + + if ($this->getVar("gallery/transition/duration")) { + $optionItems['transitionduration'] = + (int)$this->escapeHtml($this->getVar("gallery/transition/duration")); + } + + $optionItems['transition'] = $this->escapeHtml($this->getVar("gallery/transition/effect")); + $optionItems['navarrows'] = $this->getVar("gallery/navarrows"); + $optionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/navtype")); + $optionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/navdir")); + + if ($this->getVar("gallery/thumbmargin")) { + $optionItems['thumbmargin'] = (int)$this->escapeHtml($this->getVar("gallery/thumbmargin")); + } + + return $this->jsonSerializer->serialize($optionItems); + } + + /** + * Retrieve gallery fullscreen options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getFSOptionsJson() + { + $fsOptionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/fullscreen/nav"))) { + $fsOptionItems['nav'] = $this->getVar("gallery/fullscreen/nav") ? 'true' : 'false'; + } else { + $fsOptionItems['nav'] = $this->escapeHtml($this->getVar("gallery/fullscreen/nav")); + } + + $fsOptionItems['loop'] = $this->getVar("gallery/fullscreen/loop"); + $fsOptionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navdir")); + $fsOptionItems['navarrows'] = $this->getVar("gallery/fullscreen/navarrows"); + $fsOptionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navtype")); + $fsOptionItems['arrows'] = $this->getVar("gallery/fullscreen/arrows"); + $fsOptionItems['showCaption'] = $this->getVar("gallery/fullscreen/caption"); + + if ($this->getVar("gallery/fullscreen/transition/duration")) { + $fsOptionItems['transitionduration'] = (int)$this->escapeHtml( + $this->getVar("gallery/fullscreen/transition/duration") + ); + } + + $fsOptionItems['transition'] = $this->escapeHtml($this->getVar("gallery/fullscreen/transition/effect")); + + if ($this->getVar("gallery/fullscreen/keyboard")) { + $fsOptionItems['keyboard'] = $this->getVar("gallery/fullscreen/keyboard"); + } + + if ($this->getVar("gallery/fullscreen/thumbmargin")) { + $fsOptionItems['thumbmargin'] = + (int)$this->escapeHtml($this->getVar("gallery/fullscreen/thumbmargin")); + } + + return $this->jsonSerializer->serialize($fsOptionItems); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options.php b/app/code/Magento/Catalog/Block/Product/View/Options.php index 0720c018f6a9b..c457b20cd0904 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options.php @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Product options block - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Product\View; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option\Value; /** + * Product options block + * + * @author Magento Core Team <core@magentocommerce.com> * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -121,6 +120,8 @@ public function setProduct(Product $product = null) } /** + * Get group of option. + * * @param string $type * @return string */ @@ -142,6 +143,8 @@ public function getOptions() } /** + * Check if block has options. + * * @return bool */ public function hasOptions() @@ -160,7 +163,10 @@ public function hasOptions() */ protected function _getPriceConfiguration($option) { - $optionPrice = $this->pricingHelper->currency($option->getPrice(true), false, false); + $optionPrice = $option->getPrice(true); + if ($option->getPriceType() !== Value::TYPE_PERCENT) { + $optionPrice = $this->pricingHelper->currency($optionPrice, false, false); + } $data = [ 'prices' => [ 'oldPrice' => [ @@ -195,7 +201,7 @@ protected function _getPriceConfiguration($option) ], ], 'type' => $option->getPriceType(), - 'name' => $option->getTitle() + 'name' => $option->getTitle(), ]; return $data; } @@ -231,7 +237,7 @@ public function getJsonConfig() //pass the return array encapsulated in an object for the other modules to be able to alter it eg: weee $this->_eventManager->dispatch('catalog_product_option_price_configuration_after', ['configObj' => $configObj]); - $config=$configObj->getConfig(); + $config = $configObj->getConfig(); return $this->_jsonEncoder->encode($config); } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index 181211a0fc4a2..059580b9b5eae 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -9,11 +9,14 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\Catalog\Block\Product\View\Options; use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface; /** + * Product aoptions section abstract block. + * * @api * @since 100.0.2 */ @@ -46,7 +49,7 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper - * @param \Magento\Catalog\Helper\Data $catalogData, + * @param \Magento\Catalog\Helper\Data $catalogData * @param array $data */ public function __construct( @@ -123,6 +126,8 @@ public function getFormattedPrice() } /** + * Retrieve formatted price. + * * @return string * * @deprecated @@ -134,7 +139,7 @@ public function getFormatedPrice() } /** - * Return formated price + * Return formatted price * * @param array $value * @param bool $flag diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php index 7df9b972e1501..d9d663b32f4de 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php @@ -3,8 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Block\Product\View\Options\Type; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Block\Product\View\Options\Type\Select\CheckableFactory; +use Magento\Catalog\Block\Product\View\Options\Type\Select\MultipleFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Catalog\Helper\Data as CatalogHelper; + /** * Product options text type block * @@ -13,169 +22,60 @@ */ class Select extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions { + /** + * @var CheckableFactory + */ + private $checkableFactory; + /** + * @var MultipleFactory + */ + private $multipleFactory; + + /** + * Select constructor. + * @param Context $context + * @param Data $pricingHelper + * @param CatalogHelper $catalogData + * @param array $data + * @param CheckableFactory|null $checkableFactory + * @param MultipleFactory|null $multipleFactory + */ + public function __construct( + Context $context, + Data $pricingHelper, + CatalogHelper $catalogData, + array $data = [], + CheckableFactory $checkableFactory = null, + MultipleFactory $multipleFactory = null + ) { + parent::__construct($context, $pricingHelper, $catalogData, $data); + $this->checkableFactory = $checkableFactory ?: ObjectManager::getInstance()->get(CheckableFactory::class); + $this->multipleFactory = $multipleFactory ?: ObjectManager::getInstance()->get(MultipleFactory::class); + } + /** * Return html for control element * * @return string - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getValuesHtml() { - $_option = $this->getOption(); - $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $_option->getId()); - $store = $this->getProduct()->getStore(); - - $this->setSkipJsReloadPrice(1); - // Remove inline prototype onclick and onchange events - - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE + $option = $this->getOption(); + $optionType = $option->getType(); + if ($optionType === Option::OPTION_TYPE_DROP_DOWN || + $optionType === Option::OPTION_TYPE_MULTIPLE ) { - $require = $_option->getIsRequire() ? ' required' : ''; - $extraParams = ''; - $select = $this->getLayout()->createBlock( - \Magento\Framework\View\Element\Html\Select::class - )->setData( - [ - 'id' => 'select_' . $_option->getId(), - 'class' => $require . ' product-custom-option admin__control-select' - ] - ); - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { - $select->setName('options[' . $_option->getId() . ']')->addOption('', __('-- Please Select --')); - } else { - $select->setName('options[' . $_option->getId() . '][]'); - $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); - } - foreach ($_option->getValues() as $_value) { - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ], - false - ); - $select->addOption( - $_value->getOptionTypeId(), - $_value->getTitle() . ' ' . strip_tags($priceStr) . '', - ['price' => $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false)] - ); - } - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { - $extraParams = ' multiple="multiple"'; - } - if (!$this->getSkipJsReloadPrice()) { - $extraParams .= ' onchange="opConfig.reloadPrice()"'; - } - $extraParams .= ' data-selector="' . $select->getName() . '"'; - $select->setExtraParams($extraParams); - - if ($configValue) { - $select->setValue($configValue); - } - - return $select->getHtml(); + $optionBlock = $this->multipleFactory->create(); } - - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX + if ($optionType === Option::OPTION_TYPE_RADIO || + $optionType === Option::OPTION_TYPE_CHECKBOX ) { - $selectHtml = '<div class="options-list nested" id="options-' . $_option->getId() . '-list">'; - $require = $_option->getIsRequire() ? ' required' : ''; - $arraySign = ''; - switch ($_option->getType()) { - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO: - $type = 'radio'; - $class = 'radio admin__control-radio'; - if (!$_option->getIsRequire()) { - $selectHtml .= '<div class="field choice admin__field admin__field-option">' . - '<input type="radio" id="options_' . - $_option->getId() . - '" class="' . - $class . - ' product-custom-option" name="options[' . - $_option->getId() . - ']"' . - ' data-selector="options[' . $_option->getId() . ']"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' value="" checked="checked" /><label class="label admin__field-label" for="options_' . - $_option->getId() . - '"><span>' . - __('None') . '</span></label></div>'; - } - break; - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX: - $type = 'checkbox'; - $class = 'checkbox admin__control-checkbox'; - $arraySign = '[]'; - break; - } - $count = 1; - foreach ($_option->getValues() as $_value) { - $count++; - - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ] - ); - - $htmlValue = $_value->getOptionTypeId(); - if ($arraySign) { - $checked = is_array($configValue) && in_array($htmlValue, $configValue) ? 'checked' : ''; - } else { - $checked = $configValue == $htmlValue ? 'checked' : ''; - } - - $dataSelector = 'options[' . $_option->getId() . ']'; - if ($arraySign) { - $dataSelector .= '[' . $htmlValue . ']'; - } - - $selectHtml .= '<div class="field choice admin__field admin__field-option' . - $require . - '">' . - '<input type="' . - $type . - '" class="' . - $class . - ' ' . - $require . - ' product-custom-option"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' name="options[' . - $_option->getId() . - ']' . - $arraySign . - '" id="options_' . - $_option->getId() . - '_' . - $count . - '" value="' . - $htmlValue . - '" ' . - $checked . - ' data-selector="' . $dataSelector . '"' . - ' price="' . - $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false) . - '" />' . - '<label class="label admin__field-label" for="options_' . - $_option->getId() . - '_' . - $count . - '"><span>' . - $_value->getTitle() . - '</span> ' . - $priceStr . - '</label>'; - $selectHtml .= '</div>'; - } - $selectHtml .= '</div>'; - - return $selectHtml; + $optionBlock = $this->checkableFactory->create(); } + return $optionBlock + ->setOption($option) + ->setProduct($this->getProduct()) + ->setSkipJsReloadPrice(1) + ->_toHtml(); } } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php new file mode 100644 index 0000000000000..3d856f85dbd94 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php @@ -0,0 +1,68 @@ +<?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\Select; + +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; +use Magento\Catalog\Model\Product\Option; + +/** + * Represent needed logic for checkbox and radio button option types + */ +class Checkable extends AbstractOptions +{ + /** + * @var string + */ + protected $_template = 'Magento_Catalog::product/composite/fieldset/options/view/checkable.phtml'; + + /** + * Returns formated price + * + * @param ProductCustomOptionValuesInterface $value + * @return string + */ + public function formatPrice(ProductCustomOptionValuesInterface $value): string + { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return parent::_formatPrice( + [ + 'is_percent' => $value->getPriceType() === 'percent', + 'pricing_value' => $value->getPrice($value->getPriceType() === 'percent') + ] + ); + } + + /** + * Returns current currency for store + * + * @param ProductCustomOptionValuesInterface $value + * @return float|string + */ + public function getCurrencyByStore(ProductCustomOptionValuesInterface $value) + { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return $this->pricingHelper->currencyByStore( + $value->getPrice(true), + $this->getProduct()->getStore(), + false + ); + } + + /** + * Returns preconfigured value for given option + * + * @param Option $option + * @return string|array|null + */ + public function getPreconfiguredValue(Option $option) + { + return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php new file mode 100644 index 0000000000000..09a931dfa0693 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php @@ -0,0 +1,112 @@ +<?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\Select; + +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\View\Element\Html\Select; + +/** + * Represent needed logic for dropdown and multi-select + */ +class Multiple extends AbstractOptions +{ + /** + * @inheritdoc + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _toHtml() + { + $option = $this->getOption(); + $optionType = $option->getType(); + $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $require = $option->getIsRequire() ? ' required' : ''; + $extraParams = ''; + /** @var Select $select */ + $select = $this->getLayout()->createBlock( + Select::class + )->setData( + [ + 'id' => 'select_' . $option->getId(), + 'class' => $require . ' product-custom-option admin__control-select' + ] + ); + $select = $this->insertSelectOption($select, $option); + $select = $this->processSelectOption($select, $option); + if ($optionType === Option::OPTION_TYPE_MULTIPLE) { + $extraParams = ' multiple="multiple"'; + } + if (!$this->getSkipJsReloadPrice()) { + $extraParams .= ' onchange="opConfig.reloadPrice()"'; + } + $extraParams .= ' data-selector="' . $select->getName() . '"'; + $select->setExtraParams($extraParams); + if ($configValue) { + $select->setValue($configValue); + } + return $select->getHtml(); + } + + /** + * Returns select with inserted option give as a parameter + * + * @param Select $select + * @param Option $option + * @return Select + */ + private function insertSelectOption(Select $select, Option $option): Select + { + $require = $option->getIsRequire() ? ' required' : ''; + if ($option->getType() === Option::OPTION_TYPE_DROP_DOWN) { + $select->setName('options[' . $option->getId() . ']')->addOption('', __('-- Please Select --')); + } else { + $select->setName('options[' . $option->getId() . '][]'); + $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); + } + + return $select; + } + + /** + * Returns select with formated option prices + * + * @param Select $select + * @param Option $option + * @return Select + */ + private function processSelectOption(Select $select, Option $option): Select + { + $store = $this->getProduct()->getStore(); + foreach ($option->getValues() as $_value) { + $isPercentPriceType = $_value->getPriceType() === 'percent'; + $priceStr = $this->_formatPrice( + [ + 'is_percent' => $isPercentPriceType, + 'pricing_value' => $_value->getPrice($isPercentPriceType) + ], + false + ); + $select->addOption( + $_value->getOptionTypeId(), + $_value->getTitle() . ' ' . strip_tags($priceStr) . '', + [ + 'price' => $this->pricingHelper->currencyByStore( + $_value->getPrice(true), + $store, + false + ) + ] + ); + } + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php index da35b566d7e71..dd2e23e67f3d7 100644 --- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php +++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php @@ -20,8 +20,8 @@ /** * Reports Viewed Products Counter * - * The main responsilibity of this class is provide necessary data to track viewed products - * by customer on frontend and data to synchornize this tracks with backend + * The main responsibility of this class is provide necessary data to track viewed products + * by customer on frontend and data to synchronize this tracks with backend * * @api * @since 101.1.0 @@ -109,6 +109,8 @@ public function __construct( * * @return string {JSON encoded data} * @since 101.1.0 + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCurrentProductData() { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php index ba6bfddca9c6c..082101ff07826 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +/** + * Move category admin controller + */ class Move extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface { /** @@ -46,7 +49,7 @@ public function __construct( /** * Move category action * - * @return \Magento\Framework\Controller\Result\Raw + * @return \Magento\Framework\Controller\Result\Json */ public function execute() { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php index 046ebbb119e5b..e3d40bee214d1 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,9 @@ use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +/** + * Class RefreshPath + */ class RefreshPath extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface { /** @@ -44,6 +46,7 @@ public function execute() 'id' => $categoryId, 'path' => $category->getPath(), 'parentId' => $category->getParentId(), + 'level' => $category->getLevel() ]); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index 11c0a73a73708..77518fd9bf5cc 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -147,6 +147,7 @@ public function execute() $parentCategory = $this->getParentCategory($parentId, $storeId); $category->setPath($parentCategory->getPath()); $category->setParentId($parentCategory->getId()); + $category->setLevel(null); } /** diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 0730e7a7c5dc1..342bbc388f872 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -6,8 +6,10 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Class Save @@ -16,75 +18,68 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute implements HttpPostActionInterface { /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var \Magento\Framework\Bulk\BulkManagementInterface */ - protected $_productFlatIndexerProcessor; + private $bulkManagement; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory */ - protected $_productPriceIndexerProcessor; + private $operationFactory; /** - * Catalog product - * - * @var \Magento\Catalog\Helper\Product + * @var \Magento\Framework\DataObject\IdentityGeneratorInterface */ - protected $_catalogProduct; + private $identityService; /** - * @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory + * @var \Magento\Framework\Serialize\SerializerInterface */ - protected $stockItemFactory; + private $serializer; /** - * Stock Indexer - * - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + * @var \Magento\Authorization\Model\UserContextInterface */ - protected $_stockIndexerProcessor; + private $userContext; /** - * @var \Magento\Framework\Api\DataObjectHelper + * @var int */ - protected $dataObjectHelper; + private $bulkSize; /** * @param Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper - * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor - * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor - * @param \Magento\Catalog\Helper\Product $catalogProduct - * @param \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory + * @param \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param int $bulkSize */ public function __construct( Action\Context $context, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, - \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, - \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, - \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, - \Magento\Catalog\Helper\Product $catalogProduct, - \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement, + \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory, + \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService, + \Magento\Framework\Serialize\SerializerInterface $serializer, + \Magento\Authorization\Model\UserContextInterface $userContext, + int $bulkSize = 100 ) { - $this->_productFlatIndexerProcessor = $productFlatIndexerProcessor; - $this->_productPriceIndexerProcessor = $productPriceIndexerProcessor; - $this->_stockIndexerProcessor = $stockIndexerProcessor; - $this->_catalogProduct = $catalogProduct; - $this->stockItemFactory = $stockItemFactory; parent::__construct($context, $attributeHelper); - $this->dataObjectHelper = $dataObjectHelper; + $this->bulkManagement = $bulkManagement; + $this->operationFactory = $operartionFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; + $this->userContext = $userContext; + $this->bulkSize = $bulkSize; } /** * Update product attributes * - * @return \Magento\Backend\Model\View\Result\Redirect - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -93,128 +88,184 @@ public function execute() } /* Collect Data */ - $inventoryData = $this->getRequest()->getParam('inventory', []); $attributesData = $this->getRequest()->getParam('attributes', []); $websiteRemoveData = $this->getRequest()->getParam('remove_website_ids', []); $websiteAddData = $this->getRequest()->getParam('add_website_ids', []); - /* Prepare inventory data item options (use config settings) */ - $options = $this->_objectManager->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) - ->getConfigItemOptions(); - foreach ($options as $option) { - if (isset($inventoryData[$option]) && !isset($inventoryData['use_config_' . $option])) { - $inventoryData['use_config_' . $option] = 0; - } - } + $storeId = $this->attributeHelper->getSelectedStoreId(); + $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); + $productIds = $this->attributeHelper->getProductIds(); + + $attributesData = $this->sanitizeProductAttributes($attributesData); try { - $storeId = $this->attributeHelper->getSelectedStoreId(); - if ($attributesData) { - $dateFormat = $this->_objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) - ->getDateFormat(\IntlDateFormatter::SHORT); - - foreach ($attributesData as $attributeCode => $value) { - $attribute = $this->_objectManager->get(\Magento\Eav\Model\Config::class) - ->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); - if (!$attribute->getAttributeId()) { - unset($attributesData[$attributeCode]); - continue; - } - if ($attribute->getBackendType() == 'datetime') { - if (!empty($value)) { - $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); - $filterInternal = new \Zend_Filter_NormalizedToLocalized( - ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] - ); - $value = $filterInternal->filter($filterInput->filter($value)); - } else { - $value = null; - } - $attributesData[$attributeCode] = $value; - } elseif ($attribute->getFrontendInput() == 'multiselect') { - // Check if 'Change' checkbox has been checked by admin for this attribute - $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); - if (!$isChanged) { - unset($attributesData[$attributeCode]); - continue; - } - if (is_array($value)) { - $value = implode(',', $value); - } - $attributesData[$attributeCode] = $value; - } - } + $this->publish($attributesData, $websiteRemoveData, $websiteAddData, $storeId, $websiteId, $productIds); + $this->messageManager->addSuccessMessage(__('Message is added to queue')); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while updating the product(s) attributes.') + ); + } - $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class) - ->updateAttributes($this->attributeHelper->getProductIds(), $attributesData, $storeId); - } + return $this->resultRedirectFactory->create()->setPath('catalog/product/', ['store' => $storeId]); + } - if ($inventoryData) { - // TODO why use ObjectManager? - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry */ - $stockRegistry = $this->_objectManager - ->create(\Magento\CatalogInventory\Api\StockRegistryInterface::class); - /** @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository */ - $stockItemRepository = $this->_objectManager - ->create(\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class); - foreach ($this->attributeHelper->getProductIds() as $productId) { - $stockItemDo = $stockRegistry->getStockItem( - $productId, - $this->attributeHelper->getStoreWebsiteId($storeId) - ); - if (!$stockItemDo->getProductId()) { - $inventoryData['product_id'] = $productId; - } - - $stockItemId = $stockItemDo->getId(); - $this->dataObjectHelper->populateWithArray( - $stockItemDo, - $inventoryData, - \Magento\CatalogInventory\Api\Data\StockItemInterface::class + /** + * Sanitize product attributes + * + * @param array $attributesData + * + * @return array + */ + private function sanitizeProductAttributes($attributesData) + { + $dateFormat = $this->_objectManager->get(TimezoneInterface::class)->getDateFormat(\IntlDateFormatter::SHORT); + $config = $this->_objectManager->get(\Magento\Eav\Model\Config::class); + + foreach ($attributesData as $attributeCode => $value) { + $attribute = $config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + if (!$attribute->getAttributeId()) { + unset($attributesData[$attributeCode]); + continue; + } + if ($attribute->getBackendType() === 'datetime') { + if (!empty($value)) { + $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); + $filterInternal = new \Zend_Filter_NormalizedToLocalized( + ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] ); - $stockItemDo->setItemId($stockItemId); - $stockItemRepository->save($stockItemDo); + $value = $filterInternal->filter($filterInput->filter($value)); + } else { + $value = null; } - $this->_stockIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); - } - - if ($websiteAddData || $websiteRemoveData) { - /* @var $actionModel \Magento\Catalog\Model\Product\Action */ - $actionModel = $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class); - $productIds = $this->attributeHelper->getProductIds(); - - if ($websiteRemoveData) { - $actionModel->updateWebsites($productIds, $websiteRemoveData, 'remove'); + $attributesData[$attributeCode] = $value; + } elseif ($attribute->getFrontendInput() === 'multiselect') { + // Check if 'Change' checkbox has been checked by admin for this attribute + $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); + if (!$isChanged) { + unset($attributesData[$attributeCode]); + continue; } - if ($websiteAddData) { - $actionModel->updateWebsites($productIds, $websiteAddData, 'add'); + if (is_array($value)) { + $value = implode(',', $value); } - - $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); + $attributesData[$attributeCode] = $value; } + } + return $attributesData; + } - $this->messageManager->addSuccessMessage( - __('A total of %1 record(s) were updated.', count($this->attributeHelper->getProductIds())) - ); - - $this->_productFlatIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); + /** + * Schedule new bulk + * + * @param array $attributesData + * @param array $websiteRemoveData + * @param array $websiteAddData + * @param int $storeId + * @param int $websiteId + * @param array $productIds + * @throws \Magento\Framework\Exception\LocalizedException + * + * @return void + */ + private function publish( + $attributesData, + $websiteRemoveData, + $websiteAddData, + $storeId, + $websiteId, + $productIds + ):void { + $productIdsChunks = array_chunk($productIds, $this->bulkSize); + $bulkUuid = $this->identityService->generateId(); + $bulkDescription = __('Update attributes for ' . count($productIds) . ' selected products'); + $operations = []; + foreach ($productIdsChunks as $productIdsChunk) { + if ($websiteRemoveData || $websiteAddData) { + $dataToUpdate = [ + 'website_assign' => $websiteAddData, + 'website_detach' => $websiteRemoveData + ]; + $operations[] = $this->makeOperation( + 'Update website assign', + 'product_action_attribute.website.update', + $dataToUpdate, + $storeId, + $websiteId, + $productIdsChunk, + $bulkUuid + ); + } - if ($this->_catalogProduct->isDataForPriceIndexerWasChanged($attributesData) - || !empty($websiteRemoveData) - || !empty($websiteAddData) - ) { - $this->_productPriceIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); + if ($attributesData) { + $operations[] = $this->makeOperation( + 'Update product attributes', + 'product_action_attribute.update', + $attributesData, + $storeId, + $websiteId, + $productIdsChunk, + $bulkUuid + ); } - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addExceptionMessage( - $e, - __('Something went wrong while updating the product(s) attributes.') + } + + if (!empty($operations)) { + $result = $this->bulkManagement->scheduleBulk( + $bulkUuid, + $operations, + $bulkDescription, + $this->userContext->getUserId() ); + if (!$result) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Something went wrong while processing the request.') + ); + } } + } + + /** + * Make asynchronous operation + * + * @param string $meta + * @param string $queue + * @param array $dataToUpdate + * @param int $storeId + * @param int $websiteId + * @param array $productIds + * @param int $bulkUuid + * + * @return OperationInterface + */ + private function makeOperation( + $meta, + $queue, + $dataToUpdate, + $storeId, + $websiteId, + $productIds, + $bulkUuid + ): OperationInterface { + $dataToEncode = [ + 'meta_information' => $meta, + 'product_ids' => $productIds, + 'store_id' => $storeId, + 'website_id' => $websiteId, + 'attributes' => $dataToUpdate + ]; + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => $queue, + 'serialized_data' => $this->serializer->serialize($dataToEncode), + 'status' => \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN, + ] + ]; - return $this->resultRedirectFactory->create() - ->setPath('catalog/product/', ['store' => $this->attributeHelper->getSelectedStoreId()]); + return $this->operationFactory->create($data); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index 39ed11b1806cd..853cc65270306 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -195,25 +195,6 @@ public function execute() ? $model->getAttributeCode() : $this->getRequest()->getParam('attribute_code'); $attributeCode = $attributeCode ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]); - if (strlen($attributeCode) > 0) { - $validatorAttrCode = new \Zend_Validate_Regex( - ['pattern' => '/^[a-zA-Z\x{600}-\x{6FF}][a-zA-Z\x{600}-\x{6FF}_0-9]{0,30}$/u'] - ); - if (!$validatorAttrCode->isValid($attributeCode)) { - $this->messageManager->addErrorMessage( - __( - 'Attribute code "%1" is invalid. Please use only letters (a-z or A-Z), ' . - 'numbers (0-9) or underscore(_) in this field, first character should be a letter.', - $attributeCode - ) - ); - return $this->returnResult( - 'catalog/*/edit', - ['attribute_id' => $attributeId, '_current' => true], - ['error' => true] - ); - } - } $data['attribute_code'] = $attributeCode; //validate frontend_input diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index 381ca5d08d82a..c74a382724a00 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -7,12 +7,13 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; -use Magento\Framework\Serialize\Serializer\FormData; +use Magento\Catalog\Controller\Adminhtml\Product\Attribute as AttributeAction; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; -use Magento\Catalog\Controller\Adminhtml\Product\Attribute as AttributeAction; +use Magento\Framework\Serialize\Serializer\FormData; /** * Product attribute validate controller. @@ -43,6 +44,11 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo */ private $formDataSerializer; + /** + * @var AttributeCodeValidator + */ + private $attributeCodeValidator; + /** * Constructor * @@ -54,6 +60,7 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param array $multipleAttributeList * @param FormData|null $formDataSerializer + * @param AttributeCodeValidator|null $attributeCodeValidator */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -63,7 +70,8 @@ public function __construct( \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Framework\View\LayoutFactory $layoutFactory, array $multipleAttributeList = [], - FormData $formDataSerializer = null + FormData $formDataSerializer = null, + AttributeCodeValidator $attributeCodeValidator = null ) { parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); $this->resultJsonFactory = $resultJsonFactory; @@ -71,6 +79,9 @@ public function __construct( $this->multipleAttributeList = $multipleAttributeList; $this->formDataSerializer = $formDataSerializer ?: ObjectManager::getInstance() ->get(FormData::class); + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( + AttributeCodeValidator::class + ); } /** @@ -105,7 +116,7 @@ public function execute() $attributeCode ); - if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type') { + if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type' || $attributeCode === 'type_id') { $message = strlen($this->getRequest()->getParam('attribute_code')) ? __('An attribute with this code already exists.') : __('An attribute with the same code (%1) already exists.', $attributeCode); @@ -115,6 +126,12 @@ public function execute() $response->setError(true); $response->setProductAttribute($attribute->toArray()); } + + if (!$this->attributeCodeValidator->isValid($attributeCode)) { + $this->setMessageToResponse($response, $this->attributeCodeValidator->getMessages()); + $response->setError(true); + } + if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ @@ -163,7 +180,7 @@ private function isUniqueAdminValues(array $optionsValues, array $deletedOptions { $adminValues = []; foreach ($optionsValues as $optionKey => $values) { - if (!(isset($deletedOptions[$optionKey]) and $deletedOptions[$optionKey] === '1')) { + if (!(isset($deletedOptions[$optionKey]) && $deletedOptions[$optionKey] === '1')) { $adminValues[] = reset($values); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php index 125406061aed7..78ad9f423871f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ProductFactory; use Magento\Cms\Model\Wysiwyg as WysiwygModel; use Magento\Framework\App\RequestInterface; @@ -15,6 +18,11 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type as ProductTypes; +/** + * Build a product based on a request + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Builder { /** @@ -79,10 +87,11 @@ public function __construct( * Build product based on user request * * @param RequestInterface $request - * @return \Magento\Catalog\Model\Product + * @return ProductInterface * @throws \RuntimeException + * @throws \Magento\Framework\Exception\LocalizedException */ - public function build(RequestInterface $request) + public function build(RequestInterface $request): ProductInterface { $productId = (int) $request->getParam('id'); $storeId = $request->getParam('store', 0); @@ -92,6 +101,9 @@ public function build(RequestInterface $request) if ($productId) { try { $product = $this->productRepository->getById($productId, true, $storeId); + if ($attributeSetId) { + $product->setAttributeSetId($attributeSetId); + } } catch (\Exception $e) { $product = $this->createEmptyProduct(ProductTypes::DEFAULT_TYPE, $attributeSetId, $storeId); $this->logger->critical($e); @@ -113,6 +125,8 @@ public function build(RequestInterface $request) } /** + * Create a product with the given properties + * * @param int $typeId * @param int $attributeSetId * @param int $storeId diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php index 40e62895caffc..51aaa8c178edd 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Get specified tab grid controller. + */ +class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory @@ -47,7 +51,7 @@ public function execute() $this->productBuilder->build($this->getRequest()); $block = $this->getRequest()->getParam('gridOnlyBlock'); - $blockClassSuffix = str_replace(' ', '_', ucwords(str_replace('_', ' ', $block))); + $blockClassSuffix = ucwords($block, '_'); /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index e84d9ff12906e..825d0ee032d6c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -159,6 +159,7 @@ public function execute() if ($redirectBack === 'duplicate') { $product->unsetData('quantity_and_stock_status'); $newProduct = $this->productCopier->copy($product); + $this->checkUniqueAttributes($product); $this->messageManager->addSuccessMessage(__('You duplicated the product.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -343,4 +344,25 @@ private function persistMediaData(ProductInterface $product, array $data) return $data; } + + /** + * Check unique attributes and add error to message manager + * + * @param \Magento\Catalog\Model\Product $product + */ + private function checkUniqueAttributes(\Magento\Catalog\Model\Product $product) + { + $uniqueLabels = []; + foreach ($product->getAttributes() as $attribute) { + if ($attribute->getIsUnique() && $attribute->getIsUserDefined() + && $product->getData($attribute->getAttributeCode()) !== null + ) { + $uniqueLabels[] = $attribute->getDefaultFrontendLabel(); + } + } + if ($uniqueLabels) { + $uniqueLabels = implode('", "', $uniqueLabels); + $this->messageManager->addErrorMessage(__('The value of attribute(s) "%1" must be unique', $uniqueLabels)); + } + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php index c7c71b2f56026..316983298a1b9 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php @@ -9,11 +9,12 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; /** * Controller to search product for ui-select component */ -class Search extends \Magento\Backend\App\Action +class Search extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -48,6 +49,8 @@ public function __construct( } /** + * Execute product search. + * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() : \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index 2088bb5ea77cd..da3d99a8d2745 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -6,14 +6,28 @@ */ namespace Magento\Catalog\Controller\Category; -use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Design; use Magento\Catalog\Model\Layer\Resolver; use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Session; +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\Controller\Result\ForwardFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\Page; use Magento\Framework\View\Result\PageFactory; -use Magento\Framework\App\Action\Action; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * View a category on storefront. Needs to be accessible by POST because of the store switching. @@ -25,41 +39,41 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter /** * Core registry * - * @var \Magento\Framework\Registry + * @var Registry */ protected $_coreRegistry = null; /** * Catalog session * - * @var \Magento\Catalog\Model\Session + * @var Session */ protected $_catalogSession; /** * Catalog design * - * @var \Magento\Catalog\Model\Design + * @var Design */ protected $_catalogDesign; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator + * @var CategoryUrlPathGenerator */ protected $categoryUrlPathGenerator; /** - * @var \Magento\Framework\View\Result\PageFactory + * @var PageFactory */ protected $resultPageFactory; /** - * @var \Magento\Framework\Controller\Result\ForwardFactory + * @var ForwardFactory */ protected $resultForwardFactory; @@ -83,28 +97,28 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter /** * Constructor * - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Catalog\Model\Design $catalogDesign - * @param \Magento\Catalog\Model\Session $catalogSession - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory - * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory + * @param Context $context + * @param Design $catalogDesign + * @param Session $catalogSession + * @param Registry $coreRegistry + * @param StoreManagerInterface $storeManager + * @param CategoryUrlPathGenerator $categoryUrlPathGenerator + * @param PageFactory $resultPageFactory + * @param ForwardFactory $resultForwardFactory * @param Resolver $layerResolver * @param CategoryRepositoryInterface $categoryRepository * @param ToolbarMemorizer|null $toolbarMemorizer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Catalog\Model\Design $catalogDesign, - \Magento\Catalog\Model\Session $catalogSession, - \Magento\Framework\Registry $coreRegistry, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, + Context $context, + Design $catalogDesign, + Session $catalogSession, + Registry $coreRegistry, + StoreManagerInterface $storeManager, + CategoryUrlPathGenerator $categoryUrlPathGenerator, PageFactory $resultPageFactory, - \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory, + ForwardFactory $resultForwardFactory, Resolver $layerResolver, CategoryRepositoryInterface $categoryRepository, ToolbarMemorizer $toolbarMemorizer = null @@ -125,7 +139,7 @@ public function __construct( /** * Initialize requested category object * - * @return \Magento\Catalog\Model\Category|bool + * @return Category|bool */ protected function _initCategory() { @@ -150,8 +164,8 @@ protected function _initCategory() 'catalog_controller_category_init_after', ['category' => $category, 'controller_action' => $this] ); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + } catch (LocalizedException $e) { + $this->_objectManager->get(LoggerInterface::class)->critical($e); return false; } @@ -161,13 +175,12 @@ protected function _initCategory() /** * Category view action * - * @return \Magento\Framework\Controller\ResultInterface - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) + * @return ResultInterface + * @throws NoSuchEntityException */ public function execute() { - if ($this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED)) { + if ($this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED)) { return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl()); } $category = $this->_initCategory(); @@ -188,29 +201,18 @@ public function execute() $page->getConfig()->setPageLayout($settings->getPageLayout()); } - $hasChildren = $category->hasChildren(); - if ($category->getIsAnchor()) { - $type = $hasChildren ? 'layered' : 'layered_without_children'; - } else { - $type = $hasChildren ? 'default' : 'default_without_children'; - } + $pageType = $this->getPageType($category); - if (!$hasChildren) { + if (!$category->hasChildren()) { // Two levels removed from parent. Need to add default page type. - $parentType = strtok($type, '_'); - $page->addPageLayoutHandles(['type' => $parentType], null, false); + $parentPageType = strtok($pageType, '_'); + $page->addPageLayoutHandles(['type' => $parentPageType], null, false); } - $page->addPageLayoutHandles(['type' => $type], null, false); + $page->addPageLayoutHandles(['type' => $pageType], null, false); $page->addPageLayoutHandles(['id' => $category->getId()]); // apply custom layout update once layout is loaded - $layoutUpdates = $settings->getLayoutUpdates(); - if ($layoutUpdates && is_array($layoutUpdates)) { - foreach ($layoutUpdates as $layoutUpdate) { - $page->addUpdate($layoutUpdate); - $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); - } - } + $this->applyLayoutUpdates($page, $settings); $page->getConfig()->addBodyClass('page-products') ->addBodyClass('categorypath-' . $this->categoryUrlPathGenerator->getUrlPath($category)) @@ -221,4 +223,40 @@ public function execute() return $this->resultForwardFactory->create()->forward('noroute'); } } + + /** + * Get page type based on category + * + * @param Category $category + * @return string + */ + private function getPageType(Category $category) : string + { + $hasChildren = $category->hasChildren(); + if ($category->getIsAnchor()) { + return $hasChildren ? 'layered' : 'layered_without_children'; + } + + return $hasChildren ? 'default' : 'default_without_children'; + } + + /** + * Apply custom layout updates + * + * @param Page $page + * @param DataObject $settings + * @return void + */ + private function applyLayoutUpdates( + Page $page, + DataObject $settings + ) { + $layoutUpdates = $settings->getLayoutUpdates(); + if ($layoutUpdates && is_array($layoutUpdates)) { + foreach ($layoutUpdates as $layoutUpdate) { + $page->addUpdate($layoutUpdate); + $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); + } + } + } } diff --git a/app/code/Magento/Catalog/Controller/Index/Index.php b/app/code/Magento/Catalog/Controller/Index/Index.php index eae3325df9fc2..bd00c97204996 100644 --- a/app/code/Magento/Catalog/Controller/Index/Index.php +++ b/app/code/Magento/Catalog/Controller/Index/Index.php @@ -5,12 +5,17 @@ */ namespace Magento\Catalog\Controller\Index; -class Index extends \Magento\Framework\App\Action\Action +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Catalog index page controller. + */ +class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * Index action * - * @return $this + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 170f1209ad9e6..9b8d0ad75a8c9 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Helper; use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\View\Element\Block\ArgumentInterface; /** * Catalog image helper @@ -14,7 +15,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ -class Image extends AbstractHelper +class Image extends AbstractHelper implements ArgumentInterface { /** * Media config node @@ -764,7 +765,7 @@ protected function getImageFile() protected function parseSize($string) { $size = explode('x', strtolower($string)); - if (sizeof($size) == 2) { + if (count($size) == 2) { return ['width' => $size[0] > 0 ? $size[0] : null, 'height' => $size[1] > 0 ? $size[1] : null]; } return false; diff --git a/app/code/Magento/Catalog/Helper/Product/Configuration.php b/app/code/Magento/Catalog/Helper/Product/Configuration.php index 9b47e29900992..5b8f6fad6e18a 100644 --- a/app/code/Magento/Catalog/Helper/Product/Configuration.php +++ b/app/code/Magento/Catalog/Helper/Product/Configuration.php @@ -55,6 +55,7 @@ class Configuration extends AbstractHelper implements ConfigurationInterface * @param \Magento\Framework\Filter\FilterManager $filter * @param \Magento\Framework\Stdlib\StringUtils $string * @param Json $serializer + * @param Escaper $escaper */ public function __construct( \Magento\Framework\App\Helper\Context $context, diff --git a/app/code/Magento/Catalog/Helper/Product/ProductList.php b/app/code/Magento/Catalog/Helper/Product/ProductList.php index fbea73a6324de..3aa6aeed3779a 100644 --- a/app/code/Magento/Catalog/Helper/Product/ProductList.php +++ b/app/code/Magento/Catalog/Helper/Product/ProductList.php @@ -42,6 +42,7 @@ class ProductList /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Framework\Registry $coreRegistry */ public function __construct( \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php index 1509e489aee3b..74f40a18971d5 100644 --- a/app/code/Magento/Catalog/Helper/Product/View.php +++ b/app/code/Magento/Catalog/Helper/Product/View.php @@ -10,7 +10,9 @@ /** * Catalog category helper + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class View extends \Magento\Framework\App\Helper\AbstractHelper { @@ -105,7 +107,7 @@ public function __construct( * * @param \Magento\Framework\View\Result\Page $resultPage * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Framework\View\Result\Page + * @return $this */ private function preparePageMetadata(ResultPage $resultPage, $product) { diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php index d3c84e69c9540..e296c8d3b8978 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php @@ -58,22 +58,38 @@ public function build(Filter $filter): string $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); // NOTE: store scope was ignored intentionally to perform search across all stores - $attributeSelect = $this->resourceConnection->getConnection() - ->select() - ->from( - [$tableAlias => $attribute->getBackendTable()], - $tableAlias . '.' . $attribute->getEntityIdField() - )->where( - $this->resourceConnection->getConnection()->prepareSqlCondition( - $tableAlias . '.' . $attribute->getIdFieldName(), - ['eq' => $attribute->getAttributeId()] - ) - )->where( - $this->resourceConnection->getConnection()->prepareSqlCondition( - $tableAlias . '.value', - [$conditionType => $conditionValue] - ) - ); + if ($conditionType == 'is_null') { + $entityResourceModel = $attribute->getEntity(); + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [Collection::MAIN_TABLE_ALIAS => $entityResourceModel->getEntityTable()], + Collection::MAIN_TABLE_ALIAS . '.' . $entityResourceModel->getEntityIdField() + )->joinLeft( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() . '=' . Collection::MAIN_TABLE_ALIAS . + '.' . $entityResourceModel->getEntityIdField() . ' AND ' . $tableAlias . '.' . + $attribute->getIdFieldName() . '=' . $attribute->getAttributeId(), + '' + )->where($tableAlias . '.value is null'); + } else { + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.' . $attribute->getIdFieldName(), + ['eq' => $attribute->getAttributeId()] + ) + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.value', + [$conditionType => $conditionValue] + ) + ); + } return $this->resourceConnection ->getConnection() @@ -86,6 +102,8 @@ public function build(Filter $filter): string } /** + * Get attribute entity by its code + * * @param string $field * @return Attribute * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php new file mode 100644 index 0000000000000..dc24a3090481e --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Backend; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\TemporaryStateExceptionInterface; +use Magento\Framework\Bulk\OperationInterface; + +/** + * Consumer for export message. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Consumer +{ + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + */ + private $productFlatIndexerProcessor; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + */ + private $productPriceIndexerProcessor; + + /** + * @var \Magento\Catalog\Helper\Product + */ + private $catalogProduct; + + /** + * @var \Magento\Catalog\Model\Product\Action + */ + private $productAction; + + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + + /** + * @var \Magento\Framework\Bulk\OperationManagementInterface + */ + private $operationManagement; + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @param \Magento\Catalog\Helper\Product $catalogProduct + * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor + * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor + * @param \Magento\Framework\Bulk\OperationManagementInterface $operationManagement + * @param \Magento\Catalog\Model\Product\Action $action + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + \Magento\Catalog\Helper\Product $catalogProduct, + \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, + \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, + \Magento\Framework\Bulk\OperationManagementInterface $operationManagement, + \Magento\Catalog\Model\Product\Action $action, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Serialize\SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->catalogProduct = $catalogProduct; + $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; + $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->productAction = $action; + $this->logger = $logger; + $this->serializer = $serializer; + $this->operationManagement = $operationManagement; + $this->entityManager = $entityManager; + } + + /** + * Process + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation + * @throws \Exception + * + * @return void + */ + public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->execute($data); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException + || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException + || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException + ) { + $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } else { + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __( + 'Sorry, something went wrong during product attributes update. Please see log for details.' + ); + } + } catch (NoSuchEntityException $e) { + $this->logger->critical($e->getMessage()); + $status = ($e instanceof TemporaryStateExceptionInterface) + ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (LocalizedException $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 product attributes update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } + + /** + * Execute + * + * @param array $data + * + * @return void + */ + private function execute($data): void + { + $this->productAction->updateAttributes($data['product_ids'], $data['attributes'], $data['store_id']); + if ($this->catalogProduct->isDataForPriceIndexerWasChanged($data['attributes'])) { + $this->productPriceIndexerProcessor->reindexList($data['product_ids']); + } + + $this->productFlatIndexerProcessor->reindexList($data['product_ids']); + } +} diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php new file mode 100644 index 0000000000000..32ba39d9afd98 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Backend; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\TemporaryStateExceptionInterface; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\EntityManager\EntityManager; + +/** + * Consumer for export message. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ConsumerWebsiteAssign +{ + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + */ + private $productFlatIndexerProcessor; + + /** + * @var \Magento\Catalog\Model\Product\Action + */ + private $productAction; + + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + */ + private $productPriceIndexerProcessor; + + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor + * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor + * @param \Magento\Catalog\Model\Product\Action $action + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, + \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, + \Magento\Catalog\Model\Product\Action $action, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Serialize\SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; + $this->productAction = $action; + $this->logger = $logger; + $this->serializer = $serializer; + $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->entityManager = $entityManager; + } + + /** + * Process + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation + * @throws \Exception + * + * @return void + */ + public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->execute($data); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException + || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException + || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException + ) { + $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __($e->getMessage()); + } else { + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __( + 'Sorry, something went wrong during product attributes update. Please see log for details.' + ); + } + } catch (NoSuchEntityException $e) { + $this->logger->critical($e->getMessage()); + $status = ($e instanceof TemporaryStateExceptionInterface) + ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (LocalizedException $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 product attributes update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } + + /** + * Update website in products + * + * @param array $productIds + * @param array $websiteRemoveData + * @param array $websiteAddData + * + * @return void + */ + private function updateWebsiteInProducts($productIds, $websiteRemoveData, $websiteAddData): void + { + if ($websiteRemoveData) { + $this->productAction->updateWebsites($productIds, $websiteRemoveData, 'remove'); + } + if ($websiteAddData) { + $this->productAction->updateWebsites($productIds, $websiteAddData, 'add'); + } + } + + /** + * Execute + * + * @param array $data + * + * @return void + */ + private function execute($data): void + { + $this->updateWebsiteInProducts( + $data['product_ids'], + $data['attributes']['website_detach'], + $data['attributes']['website_assign'] + ); + $this->productPriceIndexerProcessor->reindexList($data['product_ids']); + $this->productFlatIndexerProcessor->reindexList($data['product_ids']); + } +} diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 29fb0d282a87c..d911bec0aaac9 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -72,11 +72,6 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements const CACHE_TAG = 'cat_c'; - /** - * Category Store Id - */ - const STORE_ID = 'store_id'; - /**#@-*/ protected $_eventPrefix = 'catalog_category'; @@ -573,8 +568,8 @@ public function getStoreIds() */ public function getStoreId() { - if ($this->hasData(self::STORE_ID)) { - return (int)$this->_getData(self::STORE_ID); + if ($this->hasData('store_id')) { + return (int)$this->_getData('store_id'); } return (int)$this->_storeManager->getStore()->getId(); } @@ -590,7 +585,7 @@ public function setStoreId($storeId) if (!is_numeric($storeId)) { $storeId = $this->_storeManager->getStore($storeId)->getId(); } - $this->setData(self::STORE_ID, $storeId); + $this->setData('store_id', $storeId); $this->getResource()->setStoreId($storeId); return $this; } @@ -1124,10 +1119,15 @@ public function reindex() } } $productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID); - if (!$productIndexer->isScheduled() - && (!empty($this->getAffectedProductIds()) || $this->dataHasChangedFor('is_anchor')) - ) { - $productIndexer->reindexList($this->getPathIds()); + + if (!empty($this->getAffectedProductIds()) + || $this->dataHasChangedFor('is_anchor') + || $this->dataHasChangedFor('is_active')) { + if (!$productIndexer->isScheduled()) { + $productIndexer->reindexList($this->getPathIds()); + } else { + $productIndexer->invalidate(); + } } } @@ -1152,13 +1152,22 @@ public function getIdentities() $identities = [ self::CACHE_TAG . '_' . $this->getId(), ]; - if (!$this->getId() || $this->hasDataChanges() - || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU) - ) { + + if ($this->hasDataChanges()) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); + } + + if ($this->dataHasChangedFor('is_anchor') || $this->dataHasChangedFor('is_active')) { + foreach ($this->getPathIds() as $id) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $id; + } + } + + if (!$this->getId() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) { $identities[] = self::CACHE_TAG; $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); } - return $identities; + return array_unique($identities); } /** diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php index 1890ea0f7d99e..20ea899a3d0d7 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php @@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $pageLayoutBuilder; + /** + * @inheritdoc + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + */ + protected $_options = null; + /** * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder */ @@ -26,14 +32,14 @@ public function __construct(\Magento\Framework\View\Model\PageLayout\Config\Buil } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllOptions() { - if (!$this->_options) { - $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); - array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); - } - return $this->_options; + $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); + array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); + $this->_options = $options; + + return $options; } } diff --git a/app/code/Magento/Catalog/Model/Category/Tree.php b/app/code/Magento/Catalog/Model/Category/Tree.php index 6080f74d5fa06..0a9cb25d7b0e5 100644 --- a/app/code/Magento/Catalog/Model/Category/Tree.php +++ b/app/code/Magento/Catalog/Model/Category/Tree.php @@ -32,27 +32,40 @@ class Tree */ protected $treeFactory; + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\TreeFactory + */ + private $treeResourceFactory; + /** * @param \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection * @param \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory + * @param \Magento\Catalog\Model\ResourceModel\Category\TreeFactory|null $treeResourceFactory */ public function __construct( \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection, - \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory + \Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory $treeFactory, + \Magento\Catalog\Model\ResourceModel\Category\TreeFactory $treeResourceFactory = null ) { $this->categoryTree = $categoryTree; $this->storeManager = $storeManager; $this->categoryCollection = $categoryCollection; $this->treeFactory = $treeFactory; + $this->treeResourceFactory = $treeResourceFactory ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class); } /** + * Get root node by category. + * * @param \Magento\Catalog\Model\Category|null $category * @return Node|null + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getRootNode($category = null) { @@ -71,13 +84,18 @@ public function getRootNode($category = null) } /** + * Get node by category. + * * @param \Magento\Catalog\Model\Category $category * @return Node + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function getNode(\Magento\Catalog\Model\Category $category) { $nodeId = $category->getId(); - $node = $this->categoryTree->loadNode($nodeId); + $categoryTree = $this->treeResourceFactory->create(); + $node = $categoryTree->loadNode($nodeId); $node->loadChildren(); $this->prepareCollection(); $this->categoryTree->addCollectionData($this->categoryCollection); @@ -85,7 +103,11 @@ protected function getNode(\Magento\Catalog\Model\Category $category) } /** + * Prepare category collection. + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function prepareCollection() { @@ -104,6 +126,8 @@ protected function prepareCollection() } /** + * Get tree by node. + * * @param \Magento\Framework\Data\Tree\Node $node * @param int $depth * @param int $currentLevel @@ -127,6 +151,8 @@ public function getTree($node, $depth = null, $currentLevel = 0) } /** + * Get node children. + * * @param \Magento\Framework\Data\Tree\Node $node * @param int $depth * @param int $currentLevel diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php index 790ea6b921fbe..cab8e013d9ba1 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -15,6 +15,9 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +/** + * Class for getting category list. + */ class CategoryList implements CategoryListInterface { /** @@ -64,7 +67,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { diff --git a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php index e2b0a91574021..10675a7b7c7e2 100644 --- a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php +++ b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\Config\CatalogClone\Media; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; + /** * Clone model for media images related config fields * @@ -26,6 +29,11 @@ class Image extends \Magento\Framework\App\Config\Value */ protected $_attributeCollectionFactory; + /** + * @var Escaper + */ + private $escaper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -36,6 +44,9 @@ class Image extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param Escaper|null $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,8 +57,10 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + Escaper $escaper = null ) { + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); @@ -71,10 +84,9 @@ public function getPrefixes() $prefixes = []; foreach ($collection as $attribute) { - /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ $prefixes[] = [ 'field' => $attribute->getAttributeCode() . '_', - 'label' => $attribute->getFrontend()->getLabel(), + 'label' => $this->escaper->escapeHtml($attribute->getFrontend()->getLabel()), ]; } diff --git a/app/code/Magento/Catalog/Model/ImageExtractor.php b/app/code/Magento/Catalog/Model/ImageExtractor.php index dcc70cbcd2a1a..1cb1f305a2209 100644 --- a/app/code/Magento/Catalog/Model/ImageExtractor.php +++ b/app/code/Magento/Catalog/Model/ImageExtractor.php @@ -20,6 +20,7 @@ class ImageExtractor implements TypeDataExtractorInterface * @param \DOMElement $mediaNode * @param string $mediaParentTag * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function process(\DOMElement $mediaNode, $mediaParentTag) { @@ -40,6 +41,12 @@ public function process(\DOMElement $mediaNode, $mediaParentTag) $nodeValue = $this->processImageBackground($attribute->nodeValue); } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') { $nodeValue = (int) $attribute->nodeValue; + } elseif ($attributeTagName === 'constrain' + || $attributeTagName === 'aspect_ratio' + || $attributeTagName === 'frame' + || $attributeTagName === 'transparency' + ) { + $nodeValue = in_array($attribute->nodeValue, [true, 1, 'true', '1'], true) ?? false; } else { $nodeValue = $attribute->nodeValue; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php index 8b952ca844bb9..1506ccf6963bf 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php @@ -8,6 +8,9 @@ use Magento\Framework\App\ResourceConnection; +/** + * Abstract action class for category flat indexers. + */ class AbstractAction { /** @@ -130,7 +133,7 @@ protected function getFlatTableStructure($tableName) $table = $this->connection->newTable( $tableName )->setComment( - sprintf("Catalog Category Flat", $tableName) + 'Catalog Category Flat' ); //Adding columns @@ -378,7 +381,7 @@ protected function getAttributeValues($entityIds, $storeId) $linkField = $this->getCategoryMetadata()->getLinkField(); foreach ($attributesType as $type) { foreach ($this->getAttributeTypeValues($type, $entityIds, $storeId) as $row) { - if (isset($row[$linkField]) && isset($row['attribute_id'])) { + if (isset($row[$linkField], $row['attribute_id'])) { $attributeId = $row['attribute_id']; if (isset($attributes[$attributeId])) { $attributeCode = $attributes[$attributeId]['attribute_code']; @@ -496,6 +499,8 @@ protected function getTableName($name) } /** + * Get category metadata instance. + * * @return \Magento\Framework\EntityManager\EntityMetadata */ private function getCategoryMetadata() @@ -509,6 +514,8 @@ private function getCategoryMetadata() } /** + * Get skip static columns instance. + * * @return array */ private function getSkipStaticColumns() diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php index f8121b55dbf99..eb59acb56c356 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php @@ -3,33 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Category\Product\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Framework\Indexer\BatchSizeManagementInterface; use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\Indexer\BatchSizeManagementInterface + * @var BatchSizeManagementInterface */ private $batchSizeManagement; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; @@ -52,25 +65,25 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio /** * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Config $config + * @param StoreManagerInterface $storeManager + * @param Config $config * @param QueryGenerator|null $queryGenerator - * @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool + * @param BatchSizeManagementInterface|null $batchSizeManagement + * @param BatchProviderInterface|null $batchProvider + * @param MetadataPool|null $metadataPool * @param int|null $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher * @param ProcessManager $processManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config, + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, QueryGenerator $queryGenerator = null, - \Magento\Framework\Indexer\BatchSizeManagementInterface $batchSizeManagement = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + BatchSizeManagementInterface $batchSizeManagement = null, + BatchProviderInterface $batchProvider = null, + MetadataPool $metadataPool = null, $batchRowsCount = null, ActiveTableSwitcher $activeTableSwitcher = null, ProcessManager $processManager = null @@ -81,15 +94,15 @@ public function __construct( $config, $queryGenerator ); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->batchSizeManagement = $batchSizeManagement ?: $objectManager->get( - \Magento\Framework\Indexer\BatchSizeManagementInterface::class + BatchSizeManagementInterface::class ); $this->batchProvider = $batchProvider ?: $objectManager->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->metadataPool = $metadataPool ?: $objectManager->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class); @@ -97,33 +110,39 @@ public function __construct( } /** + * Create the store tables + * * @return void */ - private function createTables() + private function createTables(): void { foreach ($this->storeManager->getStores() as $store) { - $this->tableMaintainer->createTablesForStore($store->getId()); + $this->tableMaintainer->createTablesForStore((int)$store->getId()); } } /** + * Truncates the replica tables + * * @return void */ - private function clearReplicaTables() + private function clearReplicaTables(): void { foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())); } } /** + * Switches the active table + * * @return void */ - private function switchTables() + private function switchTables(): void { $tablesToSwitch = []; foreach ($this->storeManager->getStores() as $store) { - $tablesToSwitch[] = $this->tableMaintainer->getMainTable($store->getId()); + $tablesToSwitch[] = $this->tableMaintainer->getMainTable((int)$store->getId()); } $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch); } @@ -133,12 +152,13 @@ private function switchTables() * * @return $this */ - public function execute() + public function execute(): self { $this->createTables(); $this->clearReplicaTables(); $this->reindex(); $this->switchTables(); + return $this; } @@ -147,7 +167,7 @@ public function execute() * * @return void */ - protected function reindex() + protected function reindex(): void { $userFunctions = []; @@ -165,9 +185,9 @@ protected function reindex() /** * Execute indexation by store * - * @param \Magento\Store\Model\Store $store + * @param Store $store */ - private function reindexStore($store) + private function reindexStore($store): void { $this->reindexRootCategory($store); $this->reindexAnchorCategories($store); @@ -177,31 +197,31 @@ private function reindexStore($store) /** * Publish data from tmp to replica table * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function publishData($store) + private function publishData($store): void { - $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable($store->getId())); + $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable((int)$store->getId())); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())) ); - $tableName = $this->tableMaintainer->getMainReplicaTable($store->getId()); + $tableName = $this->tableMaintainer->getMainReplicaTable((int)$store->getId()); $this->connection->query( $this->connection->insertFromSelect( $select, $tableName, $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * {@inheritdoc} + * @inheritdoc */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store): void { if ($this->isIndexRootCategoryNeeded()) { $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store); @@ -211,10 +231,10 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store) /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store): void { $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -222,10 +242,10 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store): void { $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -233,40 +253,42 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex categories using given SQL select and condition. * - * @param \Magento\Framework\DB\Select $basicSelect + * @param Select $basicSelect * @param string $whereCondition - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition, $store) + private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $store): void { - $this->tableMaintainer->createMainTmpTable($store->getId()); + $this->tableMaintainer->createMainTmpTable((int)$store->getId()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainTmpTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId())) ); $this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount); - $batches = $this->batchProvider->getBatches( - $this->connection, - $entityMetadata->getEntityTable(), + + $select = $this->connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->prepareSelectsByRange( + $select, $entityMetadata->getIdentifierField(), - $this->batchRowsCount + (int)$this->batchRowsCount ); - foreach ($batches as $batch) { - $this->connection->delete($this->tableMaintainer->getMainTmpTable($store->getId())); + + foreach ($batchQueries as $query) { + $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); + $entityIds = $this->connection->fetchCol($query); $resultSelect = clone $basicSelect; - $select = $this->connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($this->connection, $select, $batch); $resultSelect->where($whereCondition, $entityIds); $this->connection->query( $this->connection->insertFromSelect( $resultSelect, - $this->tableMaintainer->getMainTmpTable($store->getId()), + $this->tableMaintainer->getMainTmpTable((int)$store->getId()), $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); $this->publishData($store); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index a218266c25034..cb708695255d4 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -17,6 +17,8 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Category rows indexer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction @@ -213,6 +215,7 @@ protected function isRangingNeeded() /** * Returns a list of category ids which are assigned to product ids in the index * + * @param array $productIds * @return \Magento\Framework\Indexer\CacheContext */ private function getCategoryIdsFromIndex(array $productIds) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php index 802176092d147..ed8f692885d91 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php @@ -7,26 +7,41 @@ namespace Magento\Catalog\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Class Full reindex action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; @@ -36,44 +51,54 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ private $scopeConfig; /** - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param DecimalFactory $eavDecimalFactory + * @param SourceFactory $eavSourceFactory + * @param MetadataPool|null $metadataPool + * @param BatchProviderInterface|null $batchProvider + * @param BatchSizeCalculator $batchSizeCalculator * @param ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig + * @param ScopeConfigInterface|null $scopeConfig + * @param QueryGenerator|null $batchQueryGenerator */ public function __construct( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null, + DecimalFactory $eavDecimalFactory, + SourceFactory $eavSourceFactory, + MetadataPool $metadataPool = null, + BatchProviderInterface $batchProvider = null, + BatchSizeCalculator $batchSizeCalculator = null, ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + QueryGenerator $batchQueryGenerator = null ) { - $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Config\ScopeConfigInterface::class + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get( + ScopeConfigInterface::class ); parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig); - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( + MetadataPool::class ); - $this->batchProvider = $batchProvider ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( + BatchProviderInterface::class ); - $this->batchSizeCalculator = $batchSizeCalculator ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class + $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( + BatchSizeCalculator::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( ActiveTableSwitcher::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get( + QueryGenerator::class + ); } /** @@ -81,10 +106,10 @@ public function __construct( * * @param array|int|null $ids * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { if (!$this->isEavIndexerEnabled()) { return; @@ -94,20 +119,21 @@ public function execute($ids = null) $connection = $indexer->getConnection(); $mainTable = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable()); $connection->truncateTable($mainTable); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName) + $select, + $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName), + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { - /** @var \Magento\Framework\DB\Select $select */ - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + foreach ($batchQueries as $query) { + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); $this->syncData($indexer, $mainTable); @@ -116,14 +142,14 @@ public function execute($ids = null) $this->activeTableSwitcher->switchTable($indexer->getConnection(), [$indexer->getMainTable()]); } } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + throw new LocalizedException(__($e->getMessage()), $e); } } /** * @inheritdoc */ - protected function syncData($indexer, $destinationTable, $ids = null) + protected function syncData($indexer, $destinationTable, $ids = null): void { $connection = $indexer->getConnection(); $connection->beginTransaction(); @@ -136,7 +162,7 @@ protected function syncData($indexer, $destinationTable, $ids = null) $select, $destinationTable, $targetColumns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ); $connection->query($query); $connection->commit(); @@ -155,7 +181,7 @@ private function isEavIndexerEnabled(): bool { $eavIndexerStatus = $this->scopeConfig->getValue( self::ENABLE_EAV_INDEXER, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); return (bool)$eavIndexerStatus; diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php index a669fb73f64fc..c14bc0dd7e507 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php @@ -54,7 +54,7 @@ public function __construct( * @param int $storeId * @param int $productId * @param string $valueFieldSuffix - * @return \Magento\Catalog\Model\Indexer\Product\Flat + * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.NPathComplexity) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php index a3d958ea537e1..e6c098ab0254e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -115,7 +115,7 @@ public function build($storeId, $changedIds, $valueFieldSuffix) /** * Create empty temporary table with given columns list * - * @param string $tableName Table name + * @param string $tableName Table name * @param array $columns array('columnName' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute, ...) * @param string $valueFieldSuffix * @@ -304,12 +304,16 @@ protected function _fillTemporaryTable( /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ foreach ($columnsList as $columnName => $attribute) { - $countTableName = 't' . $iterationNum++; + $countTableName = 't' . ($iterationNum++); $joinCondition = sprintf( - 'e.%3$s = %1$s.%3$s AND %1$s.attribute_id = %2$d AND %1$s.store_id = 0', + 'e.%3$s = %1$s.%3$s' . + ' AND %1$s.attribute_id = %2$d' . + ' AND (%1$s.store_id = %4$d' . + ' OR %1$s.store_id = 0)', $countTableName, $attribute->getId(), - $metadata->getLinkField() + $metadata->getLinkField(), + $storeId ); $select->joinLeft( @@ -323,9 +327,10 @@ protected function _fillTemporaryTable( $columnValueName = $attributeCode . $valueFieldSuffix; if (isset($flatColumns[$columnValueName])) { $valueJoinCondition = sprintf( - 'e.%1$s = %2$s.option_id AND %2$s.store_id = 0', + 'e.%1$s = %2$s.option_id AND (%2$s.store_id = %3$d OR %2$s.store_id = 0)', $attributeCode, - $countTableName + $countTableName, + $storeId ); $selectValue->joinLeft( [ diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index 1a75751570658..858eba3ab217a 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -3,41 +3,64 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Price\Action; +use Magento\Catalog\Model\Indexer\Product\Price\AbstractAction; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIterator; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Indexer\Model\ProcessManager; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var ActiveTableSwitcher */ private $activeTableSwitcher; @@ -47,54 +70,61 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction private $productMetaDataCached; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + * @var DimensionCollectionFactory */ private $dimensionCollectionFactory; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer + * @var TableMaintainer */ private $dimensionTableMaintainer; /** - * @var \Magento\Indexer\Model\ProcessManager + * @var ProcessManager */ private $processManager; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $config - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory|null $dimensionCollectionFactory - * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|null $dimensionTableMaintainer - * @param \Magento\Indexer\Model\ProcessManager $processManager + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory + * @param DefaultPrice $defaultIndexerResource + * @param MetadataPool|null $metadataPool + * @param BatchSizeCalculator|null $batchSizeCalculator + * @param BatchProviderInterface|null $batchProvider + * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $dimensionTableMaintainer + * @param ProcessManager $processManager + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $config, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $dimensionTableMaintainer = null, - \Magento\Indexer\Model\ProcessManager $processManager = null + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, + DefaultPrice $defaultIndexerResource, + MetadataPool $metadataPool = null, + BatchSizeCalculator $batchSizeCalculator = null, + BatchProviderInterface $batchProvider = null, + ActiveTableSwitcher $activeTableSwitcher = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $dimensionTableMaintainer = null, + ProcessManager $processManager = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $config, @@ -107,26 +137,27 @@ public function __construct( $defaultIndexerResource ); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator::class + BatchSizeCalculator::class ); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class + ActiveTableSwitcher::class ); $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + DimensionCollectionFactory::class ); $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class + TableMaintainer::class ); $this->processManager = $processManager ?: ObjectManager::getInstance()->get( - \Magento\Indexer\Model\ProcessManager::class + ProcessManager::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?? ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -137,13 +168,13 @@ public function __construct( * @throws \Exception * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { try { //Prepare indexer tables before full reindex $this->prepareTables(); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */ + /** @var DefaultPrice $indexer */ foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) { if ($priceIndexer instanceof DimensionalIndexerInterface) { //New price reindex mechanism @@ -170,7 +201,7 @@ public function execute($ids = null) * @return void * @throws \Exception */ - private function prepareTables() + private function prepareTables(): void { $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false); @@ -185,7 +216,7 @@ private function prepareTables() * @return void * @throws \Exception */ - private function truncateReplicaTables() + private function truncateReplicaTables(): void { foreach ($this->dimensionCollectionFactory->create() as $dimension) { $dimensionTable = $this->dimensionTableMaintainer->getMainReplicaTable($dimension); @@ -202,12 +233,12 @@ private function truncateReplicaTables() * @return void * @throws \Exception */ - private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId) + private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId): void { $userFunctions = []; foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) { - return $this->reindexByBatches($priceIndexer, $dimensions, $typeId); + $this->reindexByBatches($priceIndexer, $dimensions, $typeId); }; } $this->processManager->execute($userFunctions); @@ -223,10 +254,13 @@ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $p * @return void * @throws \Exception */ - private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId) - { + private function reindexByBatches( + DimensionalIndexerInterface $priceIndexer, + array $dimensions, + string $typeId + ): void { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions, $typeId); + $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions); } } @@ -235,16 +269,20 @@ private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, arr * * @param string $typeId * - * @return \Generator + * @return BatchIterator * @throws \Exception */ - private function getBatchesForIndexer(string $typeId) + private function getBatchesForIndexer(string $typeId): BatchIterator { $connection = $this->_defaultIndexerResource->getConnection(); - return $this->batchProvider->getBatches( - $connection, - $this->getProductMetaData()->getEntityTable(), + $entityMetadata = $this->getProductMetaData(); + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + return $this->batchQueryGenerator->generate( $this->getProductMetaData()->getIdentifierField(), + $select, $this->batchSizeCalculator->estimateBatchSize( $connection, $typeId @@ -256,20 +294,18 @@ private function getBatchesForIndexer(string $typeId) * Reindex by batch for new 'Dimensional' price indexer * * @param DimensionalIndexerInterface $priceIndexer - * @param array $batch + * @param Select $batchQuery * @param array $dimensions - * @param string $typeId * * @return void * @throws \Exception */ private function reindexByBatchWithDimensions( DimensionalIndexerInterface $priceIndexer, - array $batch, - array $dimensions, - string $typeId - ) { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + Select $batchQuery, + array $dimensions + ): void { + $entityIds = $this->getEntityIdsFromBatch($batchQuery); if (!empty($entityIds)) { $this->dimensionTableMaintainer->createMainTmpTable($dimensions); @@ -295,10 +331,10 @@ private function reindexByBatchWithDimensions( * @return void * @throws \Exception */ - private function reindexProductType(PriceInterface $priceIndexer, string $typeId) + private function reindexProductType(PriceInterface $priceIndexer, string $typeId): void { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexBatch($priceIndexer, $batch, $typeId); + $this->reindexBatch($priceIndexer, $batch); } } @@ -306,15 +342,13 @@ private function reindexProductType(PriceInterface $priceIndexer, string $typeId * Reindex by batch for old price indexer * * @param PriceInterface $priceIndexer - * @param array $batch - * @param string $typeId - * + * @param Select $batch * @return void * @throws \Exception */ - private function reindexBatch(PriceInterface $priceIndexer, array $batch, string $typeId) + private function reindexBatch(PriceInterface $priceIndexer, Select $batch): void { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + $entityIds = $this->getEntityIdsFromBatch($batch); if (!empty($entityIds)) { // Temporary table will created if not exists @@ -339,27 +373,15 @@ private function reindexBatch(PriceInterface $priceIndexer, array $batch, string /** * Get Entity Ids from batch * - * @param string $typeId - * @param array $batch - * + * @param Select $batch * @return array * @throws \Exception */ - private function getEntityIdsFromBatch(string $typeId, array $batch) + private function getEntityIdsFromBatch(Select $batch): array { $connection = $this->_defaultIndexerResource->getConnection(); - // Get entity ids from batch - $select = $connection - ->select() - ->distinct(true) - ->from( - ['e' => $this->getProductMetaData()->getEntityTable()], - $this->getProductMetaData()->getIdentifierField() - ) - ->where('type_id = ?', $typeId); - - return $this->batchProvider->getBatchIds($connection, $select, $batch); + return $connection->fetchCol($batch); } /** @@ -368,7 +390,7 @@ private function getEntityIdsFromBatch(string $typeId, array $batch) * @return EntityMetadataInterface * @throws \Exception */ - private function getProductMetaData() + private function getProductMetaData(): EntityMetadataInterface { if ($this->productMetaDataCached === null) { $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class); @@ -383,7 +405,7 @@ private function getProductMetaData() * @return string * @throws \Exception */ - private function getReplicaTable() + private function getReplicaTable(): string { return $this->activeTableSwitcher->getAdditionalTableName( $this->_defaultIndexerResource->getMainTable() @@ -394,8 +416,9 @@ private function getReplicaTable() * Replacement of tables from replica to main * * @return void + * @throws \Zend_Db_Statement_Exception */ - private function switchTables() + private function switchTables(): void { // Switch dimension tables $mainTablesByDimension = []; @@ -417,13 +440,14 @@ private function switchTables() /** * Move data from old price indexer mechanism to new indexer mechanism by dimensions. + * * Used only for backward compatibility * * @param array $dimensions - * * @return void + * @throws \Zend_Db_Statement_Exception */ - private function moveDataFromReplicaTableToReplicaTables(array $dimensions) + private function moveDataFromReplicaTableToReplicaTables(array $dimensions): void { if (!$dimensions) { return; @@ -455,17 +479,17 @@ private function moveDataFromReplicaTableToReplicaTables(array $dimensions) $select, $replicaTablesByDimension, [], - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * @deprecated + * Retrieves the index table that should be used * - * @inheritdoc + * @deprecated */ - protected function getIndexTargetTable() + protected function getIndexTargetTable(): string { return $this->activeTableSwitcher->getAdditionalTableName($this->_defaultIndexerResource->getMainTable()); } diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php b/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php index dac2632ff6db8..d76711cb21dbf 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php @@ -32,8 +32,8 @@ class Decimal extends \Magento\Catalog\Model\Layer\Filter\AbstractFilter * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Layer $layer * @param \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder - * @param \Magento\Catalog\Model\ResourceModel\Layer\Filter\DecimalFactory $filterDecimalFactory * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Catalog\Model\Layer\Filter\DataProvider\DecimalFactory $dataProviderFactory * @param array $data */ public function __construct( diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php b/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php index 4d2878b0b1e84..07c9c2eaa2491 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ +namespace Magento\Catalog\Model\Layer\Filter\Item; + /** * Item Data Builder */ -namespace Magento\Catalog\Model\Layer\Filter\Item; - class DataBuilder { /** @@ -29,7 +29,7 @@ class DataBuilder * Add Item Data * * @param string $label - * @param string $label + * @param string $value * @param int $count * @return void */ diff --git a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php index c88215d92357e..f51b2e4f90a64 100644 --- a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php +++ b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php @@ -7,6 +7,9 @@ */ namespace Magento\Catalog\Model\Plugin\ProductRepository; +/** + * Transaction wrapper for product repository CRUD. + */ class TransactionWrapper { /** @@ -24,8 +27,10 @@ public function __construct( } /** + * Transaction wrapper for save action. + * * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject - * @param callable $proceed + * @param \Closure $proceed * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param bool $saveOptions * @return \Magento\Catalog\Api\Data\ProductInterface @@ -51,8 +56,10 @@ public function aroundSave( } /** + * Transaction wrapper for delete action. + * * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject - * @param callable $proceed + * @param \Closure $proceed * @param \Magento\Catalog\Api\Data\ProductInterface $product * @return bool * @throws \Exception @@ -76,8 +83,10 @@ public function aroundDelete( } /** + * Transaction wrapper for delete by id action. + * * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject - * @param callable $proceed + * @param \Closure $proceed * @param string $productSku * @return bool * @throws \Exception diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 5d47e85768fad..1e774e45df41f 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -71,11 +71,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements */ const STORE_ID = 'store_id'; - /** - * Product Url path. - */ - const URL_PATH = 'url_path'; - /** * @var string */ diff --git a/app/code/Magento/Catalog/Model/Product/Action.php b/app/code/Magento/Catalog/Model/Product/Action.php index f78048424b42c..3863cf2457247 100644 --- a/app/code/Magento/Catalog/Model/Product/Action.php +++ b/app/code/Magento/Catalog/Model/Product/Action.php @@ -168,5 +168,7 @@ public function updateWebsites($productIds, $websiteIds, $type) if (!$categoryIndexer->isScheduled()) { $categoryIndexer->reindexList(array_unique($productIds)); } + + $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php index e346c912dccaa..db967052cb7a5 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php @@ -165,19 +165,6 @@ protected function modifyPriceData($object, $data) /** @var \Magento\Catalog\Model\Product $object */ $data = parent::modifyPriceData($object, $data); $price = $object->getPrice(); - - $specialPrice = $object->getSpecialPrice(); - $specialPriceFromDate = $object->getSpecialFromDate(); - $specialPriceToDate = $object->getSpecialToDate(); - $today = time(); - - if ($specialPrice && ($object->getPrice() > $object->getFinalPrice())) { - if ($today >= strtotime($specialPriceFromDate) && $today <= strtotime($specialPriceToDate) || - $today >= strtotime($specialPriceFromDate) && $specialPriceToDate === null) { - $price = $specialPrice; - } - } - foreach ($data as $key => $tierPrice) { $percentageValue = $this->getPercentage($tierPrice); if ($percentageValue) { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php index 2bb10d3b31a24..893000544a728 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php @@ -113,27 +113,28 @@ private function customizeAttributeCode($meta) */ private function customizeFrontendLabels($meta) { + $labelConfigs = []; + foreach ($this->storeRepository->getList() as $store) { $storeId = $store->getId(); if (!$storeId) { continue; } - - $meta['manage-titles']['children'] = [ - 'frontend_label[' . $storeId . ']' => $this->arrayManager->set( - 'arguments/data/config', - [], - [ - 'formElement' => Input::NAME, - 'componentType' => Field::NAME, - 'label' => $store->getName(), - 'dataType' => Text::NAME, - 'dataScope' => 'frontend_label[' . $storeId . ']' - ] - ), - ]; + $labelConfigs['frontend_label[' . $storeId . ']'] = $this->arrayManager->set( + 'arguments/data/config', + [], + [ + 'formElement' => Input::NAME, + 'componentType' => Field::NAME, + 'label' => $store->getName(), + 'dataType' => Text::NAME, + 'dataScope' => 'frontend_label[' . $storeId . ']' + ] + ); } + $meta['manage-titles']['children'] = $labelConfigs; + return $meta; } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php index 63b1444d1db07..dbc7535dccfa9 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php @@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $pageLayoutBuilder; + /** + * @inheritdoc + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + */ + protected $_options = null; + /** * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder */ @@ -26,14 +32,14 @@ public function __construct(\Magento\Framework\View\Model\PageLayout\Config\Buil } /** - * @return array + * @inheritdoc */ public function getAllOptions() { - if (!$this->_options) { - $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); - array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); - } - return $this->_options; + $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); + array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); + $this->_options = $options; + + return $options; } } diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index ce6b4d98bbc9f..44ebdf0f1f283 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -1,7 +1,5 @@ <?php /** - * Catalog product copier. Creates product duplicate - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,7 +9,11 @@ use Magento\Catalog\Model\Product; /** - * The copier creates product duplicates. + * Catalog product copier. + * + * Creates product duplicate. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Copier { @@ -74,22 +76,9 @@ public function copy(Product $product) $duplicate->setUpdatedAt(null); $duplicate->setId(null); $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); - $this->copyConstructor->build($product, $duplicate); - $isDuplicateSaved = false; - do { - $urlKey = $duplicate->getUrlKey(); - $urlKey = preg_match('/(.*)-(\d+)$/', $urlKey, $matches) - ? $matches[1] . '-' . ($matches[2] + 1) - : $urlKey . '-1'; - $duplicate->setUrlKey($urlKey); - $duplicate->setData(Product::URL_PATH, null); - try { - $duplicate->save(); - $isDuplicateSaved = true; - } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { - } - } while (!$isDuplicateSaved); + $this->setDefaultUrl($product, $duplicate); + $this->setStoresUrl($product, $duplicate); $this->getOptionRepository()->duplicate($product, $duplicate); $product->getResource()->duplicate( $product->getData($metadata->getLinkField()), @@ -98,6 +87,81 @@ public function copy(Product $product) return $duplicate; } + /** + * Set default URL. + * + * @param Product $product + * @param Product $duplicate + * @return void + */ + private function setDefaultUrl(Product $product, Product $duplicate) : void + { + $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); + $resource = $product->getResource(); + $attribute = $resource->getAttribute('url_key'); + $productId = $product->getId(); + $urlKey = $resource->getAttributeRawValue($productId, 'url_key', \Magento\Store\Model\Store::DEFAULT_STORE_ID); + do { + $urlKey = $this->modifyUrl($urlKey); + $duplicate->setUrlKey($urlKey); + } while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate)); + $duplicate->setData('url_path', null); + $duplicate->save(); + } + + /** + * Set URL for each store. + * + * @param Product $product + * @param Product $duplicate + * @return void + */ + private function setStoresUrl(Product $product, Product $duplicate) : void + { + $storeIds = $duplicate->getStoreIds(); + $productId = $product->getId(); + $productResource = $product->getResource(); + $defaultUrlKey = $productResource->getAttributeRawValue( + $productId, + 'url_key', + \Magento\Store\Model\Store::DEFAULT_STORE_ID + ); + $duplicate->setData('save_rewrites_history', false); + foreach ($storeIds as $storeId) { + $isDuplicateSaved = false; + $duplicate->setStoreId($storeId); + $urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId); + if ($urlKey === $defaultUrlKey) { + continue; + } + do { + $urlKey = $this->modifyUrl($urlKey); + $duplicate->setUrlKey($urlKey); + $duplicate->setData('url_path', null); + try { + $duplicate->save(); + $isDuplicateSaved = true; + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { + } + } while (!$isDuplicateSaved); + } + $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); + } + + /** + * Modify URL key. + * + * @param string $urlKey + * @return string + */ + private function modifyUrl(string $urlKey) : string + { + return preg_match('/(.*)-(\d+)$/', $urlKey, $matches) + ? $matches[1] . '-' . ($matches[2] + 1) + : $urlKey . '-1'; + } + /** * Returns product option repository. * diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 65111979c5d3a..e06e85e90a2d8 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -206,7 +206,7 @@ public function execute($product, $arguments = []) } /** - * Returns media gallery atribute instance + * Returns media gallery attribute instance * * @return \Magento\Catalog\Api\Data\ProductAttributeInterface * @since 101.0.0 @@ -230,6 +230,7 @@ public function getAttribute() * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @since 101.0.0 + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ protected function processDeletedImages($product, array &$images) { @@ -318,7 +319,7 @@ protected function duplicate($product) $this->resourceModel->duplicate( $this->getAttribute()->getAttributeId(), - isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], + $mediaGalleryData['duplicate'] ?? [], $product->getOriginalLinkId(), $product->getData($this->metadata->getLinkField()) ); @@ -400,6 +401,7 @@ protected function getUniqueFileName($file, $forTmp = false) $destinationFile = $forTmp ? $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getTmpMediaPath($file)) : $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getMediaPath($file)); + // phpcs:disable Magento2.Functions.DiscouragedFunction $destFile = dirname($file) . '/' . FileUploader::getNewFileName($destinationFile); } @@ -420,6 +422,7 @@ protected function copyImage($file) $destinationFile = $this->getUniqueFileName($file); if (!$this->mediaDirectory->isFile($this->mediaConfig->getMediaPath($file))) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception(); } @@ -437,6 +440,7 @@ protected function copyImage($file) } return str_replace('\\', '/', $destinationFile); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { $file = $this->mediaConfig->getMediaPath($file); throw new \Magento\Framework\Exception\LocalizedException( diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php index f6be7f7392b5e..4a55714a27ec5 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php +++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php @@ -161,9 +161,7 @@ private function getWatermark(string $type): array */ private function hasDefaultFrame(): bool { - return (bool) $this->viewConfig->getViewConfig()->getVarValue( - 'Magento_Catalog', - 'product_image_white_borders' - ); + return (bool) $this->viewConfig->getViewConfig(['area' => \Magento\Framework\App\Area::AREA_FRONTEND]) + ->getVarValue('Magento_Catalog', 'product_image_white_borders'); } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php index 9dc9695daffd1..bb4e247de32db 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php @@ -14,6 +14,8 @@ use Magento\Framework\App\ObjectManager; /** + * Product custom options repository + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface @@ -83,7 +85,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -92,7 +94,7 @@ public function getList($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function getProductOptions(ProductInterface $product, $requiredOnly = false) { @@ -104,7 +106,7 @@ public function getProductOptions(ProductInterface $product, $requiredOnly = fal } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $optionId) { @@ -117,7 +119,7 @@ public function get($sku, $optionId) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $entity) { @@ -126,7 +128,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $e } /** - * {@inheritdoc} + * @inheritdoc */ public function duplicate( \Magento\Catalog\Api\Data\ProductInterface $product, @@ -142,7 +144,7 @@ public function duplicate( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $option) { @@ -184,7 +186,7 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteByIdentifier($sku, $optionId) { @@ -209,8 +211,8 @@ public function deleteByIdentifier($sku, $optionId) /** * Mark original values for removal if they are absent among new values * - * @param $newValues array - * @param $originalValues \Magento\Catalog\Model\Product\Option\Value[] + * @param array $newValues + * @param \Magento\Catalog\Model\Product\Option\Value[] $originalValues * @return array */ protected function markRemovedValues($newValues, $originalValues) @@ -234,6 +236,8 @@ protected function markRemovedValues($newValues, $originalValues) } /** + * Get hydrator pool + * * @return \Magento\Framework\EntityManager\HydratorPool * @deprecated 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index c4a2d60414a7b..9cb6cda4d0a09 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository; @@ -28,6 +30,8 @@ public function __construct( } /** + * Perform action on relation/extension attribute + * * @param object $entity * @param array $arguments * @return \Magento\Catalog\Api\Data\ProductInterface|object @@ -35,6 +39,10 @@ public function __construct( */ public function execute($entity, $arguments = []) { + if ($entity->getOptionsSaved()) { + return $entity; + } + $options = $entity->getOptions(); $optionIds = []; @@ -52,11 +60,26 @@ public function execute($entity, $arguments = []) } } if ($options) { - foreach ($options as $option) { - $this->optionRepository->save($option); - } + $this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), (string)$entity->getSku()); } return $entity; } + + /** + * Save custom options + * + * @param array $options + * @param bool $hasChangedSku + * @param string $newSku + */ + private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku) + { + foreach ($options as $option) { + if ($hasChangedSku && $option->hasData('product_sku')) { + $option->setProductSku($newSku); + } + $this->optionRepository->save($option); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index b19906ecd6cc9..2b4739ebeb736 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -12,6 +12,7 @@ * Catalog product option date type * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Date extends \Magento\Catalog\Model\Product\Option\Type\DefaultType { @@ -147,7 +148,6 @@ public function validateUserValue($values) public function prepareForCart() { if ($this->getIsValid() && $this->getUserValue() !== null) { - $option = $this->getOption(); $value = $this->getUserValue(); if (isset($value['date_internal']) && $value['date_internal'] != '') { diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index 4a257a4781063..d88dd58362896 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -71,7 +71,7 @@ public function validateUserValue($values) } if (!$this->_isSingleSelection()) { $valuesCollection = $option->getOptionValuesByOptionId($value, $this->getProduct()->getStoreId())->load(); - $valueCount = is_array($value) ? count($value) : 1; + $valueCount = is_array($value) ? count($value) : 0; if ($valuesCollection->count() != $valueCount) { $this->setIsValid(false); throw new LocalizedException( diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php index 99d5016f5cdb9..08455430ccac8 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php @@ -28,13 +28,20 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator */ protected $priceTypes; + /** + * @var \Magento\Framework\Locale\FormatInterface + */ + private $localeFormat; + /** * @param \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig * @param \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig + * @param \Magento\Framework\Locale\FormatInterface|null $localeFormat */ public function __construct( \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig, - \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig + \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig, + \Magento\Framework\Locale\FormatInterface $localeFormat = null ) { foreach ($productOptionConfig->getAll() as $option) { foreach ($option['types'] as $type) { @@ -45,6 +52,9 @@ public function __construct( foreach ($priceConfig->toOptionArray() as $item) { $this->priceTypes[] = $item['value']; } + + $this->localeFormat = $localeFormat ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Locale\FormatInterface::class); } /** @@ -137,11 +147,11 @@ protected function validateOptionType(Option $option) */ protected function validateOptionValue(Option $option) { - return $this->isInRange($option->getPriceType(), $this->priceTypes); + return $this->isInRange($option->getPriceType(), $this->priceTypes) && $this->isNumber($option->getPrice()); } /** - * Check whether value is empty + * Check whether the value is empty * * @param mixed $value * @return bool @@ -152,7 +162,7 @@ protected function isEmpty($value) } /** - * Check whether value is in range + * Check whether the value is in range * * @param string $value * @param array $range @@ -164,13 +174,24 @@ protected function isInRange($value, array $range) } /** - * Check whether value is not negative + * Check whether the value is negative * * @param string $value * @return bool */ protected function isNegative($value) { - return (int) $value < 0; + return $this->localeFormat->getNumber($value) < 0; + } + + /** + * Check whether the value is a number + * + * @param string $value + * @return bool + */ + public function isNumber($value) + { + return is_numeric($this->localeFormat->getNumber($value)); } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php index 44756890b6ed7..209531f599811 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\Product\Option; +/** + * Select validator class + */ class Select extends DefaultValidator { /** @@ -83,7 +86,7 @@ protected function isValidOptionPrice($priceType, $price, $storeId) if (!$priceType && !$price) { return true; } - if (!$this->isInRange($priceType, $this->priceTypes)) { + if (!$this->isInRange($priceType, $this->priceTypes) || !$this->isNumber($price)) { return false; } diff --git a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php index 3ec8e968aa245..24775a791e59f 100644 --- a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -16,6 +16,8 @@ use Magento\Framework\EntityManager\EntityManager; /** + * A Product Widget Synchronizer. + * * Service which allows to sync product widget information, such as product id with db. In order to reuse this info * on different devices */ @@ -85,9 +87,10 @@ public function __construct( } /** - * Find lifetime in configuration. Configuration is hold in Stores Configuration - * Also this configuration is generated by: - * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration + * Finds lifetime in configuration. + * + * Configuration is hold in Stores Configuration. Also this configuration is generated by + * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} * * @param string $namespace * @return int @@ -108,6 +111,8 @@ private function getLifeTimeByNamespace($namespace) } /** + * Filters actions. + * * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that * can be persisted in database. * @@ -138,7 +143,9 @@ private function getProductIdsByActions(array $actions) $productIds = []; foreach ($actions as $action) { - $productIds[] = $action['product_id']; + if (isset($action['product_id'])) { + $productIds[] = $action['product_id']; + } } return $productIds; @@ -159,33 +166,37 @@ public function syncActions(array $productsData, $typeId) $customerId = $this->session->getCustomerId(); $visitorId = $this->visitor->getId(); $collection = $this->getActionsByType($typeId); - $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); - - /** - * Note that collection is also filtered by visitor id and customer id - * This collection shouldn't be flushed when visitor has products and then login - * It can remove only products for visitor, or only products for customer - * - * ['product_id' => 'added_at'] - * @var ProductFrontendActionInterface $item - */ - foreach ($collection as $item) { - $this->entityManager->delete($item); - } - - foreach ($productsData as $productId => $productData) { - /** @var ProductFrontendActionInterface $action */ - $action = $this->productFrontendActionFactory->create([ - 'data' => [ - 'visitor_id' => $customerId ? null : $visitorId, - 'customer_id' => $this->session->getCustomerId(), - 'added_at' => $productData['added_at'], - 'product_id' => $productId, - 'type_id' => $typeId - ] - ]); - - $this->entityManager->save($action); + $productIds = $this->getProductIdsByActions($productsData); + + if ($productIds) { + $collection->addFieldToFilter('product_id', $productIds); + + /** + * Note that collection is also filtered by visitor id and customer id + * This collection shouldn't be flushed when visitor has products and then login + * It can remove only products for visitor, or only products for customer + * + * ['product_id' => 'added_at'] + * @var ProductFrontendActionInterface $item + */ + foreach ($collection as $item) { + $this->entityManager->delete($item); + } + + foreach ($productsData as $productId => $productData) { + /** @var ProductFrontendActionInterface $action */ + $action = $this->productFrontendActionFactory->create([ + 'data' => [ + 'visitor_id' => $customerId ? null : $visitorId, + 'customer_id' => $this->session->getCustomerId(), + 'added_at' => $productData['added_at'], + 'product_id' => $productId, + 'type_id' => $typeId + ] + ]); + + $this->entityManager->save($action); + } } } diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php new file mode 100644 index 0000000000000..dabfdb74f0118 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Type; + +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product\Price\SpecialPrice; +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Store\Api\Data\WebsiteInterface; + +/** + * Product special price model. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @deprecated + * @see \Magento\Catalog\Model\Product\Type\Price + */ +class FrontSpecialPrice extends Price +{ + /** + * @var SpecialPrice + */ + private $specialPrice; + + /** + * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory + * @param \Magento\Framework\App\Config\ScopeConfigInterface $config + * @param SpecialPrice $specialPrice + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, + \Magento\Customer\Model\Session $customerSession, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + \Magento\Customer\Api\GroupManagementInterface $groupManagement, + \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory, + \Magento\Framework\App\Config\ScopeConfigInterface $config, + SpecialPrice $specialPrice + ) { + $this->specialPrice = $specialPrice; + parent::__construct( + $ruleFactory, + $storeManager, + $localeDate, + $customerSession, + $eventManager, + $priceCurrency, + $groupManagement, + $tierPriceFactory, + $config + ); + } + + /** + * @inheritdoc + * + * @deprecated + */ + protected function _applySpecialPrice($product, $finalPrice) + { + if (!$product->getSpecialPrice()) { + return $finalPrice; + } + + $specialPrices = $this->getSpecialPrices($product); + $specialPrice = !(empty($specialPrices)) ? min($specialPrices) : $product->getSpecialPrice(); + + $specialPrice = $this->calculateSpecialPrice( + $finalPrice, + $specialPrice, + $product->getSpecialFromDate(), + $product->getSpecialToDate(), + WebsiteInterface::ADMIN_CODE + ); + $product->setData('special_price', $specialPrice); + + return $specialPrice; + } + + /** + * Get special prices. + * + * @param mixed $product + * @return array + */ + private function getSpecialPrices($product): array + { + $allSpecialPrices = $this->specialPrice->get([$product->getSku()]); + $specialPrices = []; + foreach ($allSpecialPrices as $price) { + if ($this->isSuitableSpecialPrice($product, $price)) { + $specialPrices[] = $price['value']; + } + } + + return $specialPrices; + } + + /** + * Price is suitable from default and current store + start and end date are equal. + * + * @param mixed $product + * @param array $price + * @return bool + */ + private function isSuitableSpecialPrice($product, array $price): bool + { + $priceStoreId = $price[Store::STORE_ID]; + if (($priceStoreId == Store::DEFAULT_STORE_ID || $product->getStoreId() == $priceStoreId) + && $price[SpecialPriceInterface::PRICE_FROM] == $product->getSpecialFromDate() + && $price[SpecialPriceInterface::PRICE_TO] == $product->getSpecialToDate()) { + return true; + } + + return false; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index f6caa299d66d7..b30624b79dd51 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -11,12 +11,14 @@ use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Store\Api\Data\WebsiteInterface; /** * Product type price model * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Price @@ -184,6 +186,8 @@ public function getFinalPrice($qty, $product) } /** + * Retrieve final price for child product + * * @param Product $product * @param float $productQty * @param Product $childProduct @@ -428,6 +432,8 @@ public function setTierPrices($product, array $tierPrices = null) } /** + * Retrieve customer group id from product + * * @param Product $product * @return int */ @@ -453,7 +459,7 @@ protected function _applySpecialPrice($product, $finalPrice) $product->getSpecialPrice(), $product->getSpecialFromDate(), $product->getSpecialToDate(), - $product->getStore() + WebsiteInterface::ADMIN_CODE ); } @@ -601,7 +607,7 @@ public function calculatePrice( $specialPrice, $specialPriceFrom, $specialPriceTo, - $sId + WebsiteInterface::ADMIN_CODE ); if ($rulePrice === false) { diff --git a/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php index e81cdedd6d370..8acb4a6593a4c 100644 --- a/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\EntityManager\Operation\ExtensionInterface; +/** + * Add websites ids to product extension attributes. + */ class ReadHandler implements ExtensionInterface { /** @@ -18,7 +21,7 @@ class ReadHandler implements ExtensionInterface /** * ReadHandler constructor. - * @param ProductWebsiteLink $resourceModel + * @param ProductWebsiteLink $productWebsiteLink */ public function __construct( ProductWebsiteLink $productWebsiteLink @@ -27,6 +30,8 @@ public function __construct( } /** + * Add website ids to product extension attributes, if no set. + * * @param ProductInterface $product * @param array $arguments * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Model/ProductRender.php b/app/code/Magento/Catalog/Model/ProductRender.php index 702c04b910d44..5efb0343cd99b 100644 --- a/app/code/Magento/Catalog/Model/ProductRender.php +++ b/app/code/Magento/Catalog/Model/ProductRender.php @@ -206,7 +206,7 @@ public function getExtensionAttributes() * Set an extension attributes object. * * @param \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes - * @return $this + * @return void */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Model/ProductRender/Image.php b/app/code/Magento/Catalog/Model/ProductRender/Image.php index 774199a0dbf0a..5e024938d37ea 100644 --- a/app/code/Magento/Catalog/Model/ProductRender/Image.php +++ b/app/code/Magento/Catalog/Model/ProductRender/Image.php @@ -9,14 +9,16 @@ use Magento\Catalog\Api\Data\ProductRender\ImageInterface; /** - * @inheritdoc + * Product image renderer model. */ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements ImageInterface { /** + * Set url to image. + * * @param string $url - * @return @return void + * @return void */ public function setUrl($url) { @@ -34,6 +36,8 @@ public function getUrl() } /** + * Retrieve image code. + * * @return string */ public function getCode() @@ -42,6 +46,8 @@ public function getCode() } /** + * Set image code. + * * @param string $code * @return void */ @@ -51,6 +57,8 @@ public function setCode($code) } /** + * Set image height. + * * @param string $height * @return void */ @@ -60,6 +68,8 @@ public function setHeight($height) } /** + * Retrieve image height. + * * @return float */ public function getHeight() @@ -68,6 +78,8 @@ public function getHeight() } /** + * Retrieve image width. + * * @return float */ public function getWidth() @@ -76,6 +88,8 @@ public function getWidth() } /** + * Set image width. + * * @param string $width * @return void */ @@ -85,6 +99,8 @@ public function setWidth($width) } /** + * Retrieve image label. + * * @return string */ public function getLabel() @@ -93,6 +109,8 @@ public function getLabel() } /** + * Set image label. + * * @param string $label * @return void */ @@ -102,6 +120,8 @@ public function setLabel($label) } /** + * Retrieve image width after image resize. + * * @return float */ public function getResizedWidth() @@ -110,6 +130,8 @@ public function getResizedWidth() } /** + * Set image width after image resize. + * * @param string $width * @return void */ @@ -119,6 +141,8 @@ public function setResizedWidth($width) } /** + * Set image height after image resize. + * * @param string $height * @return void */ @@ -128,6 +152,8 @@ public function setResizedHeight($height) } /** + * Retrieve image height after image resize. + * * @return float */ public function getResizedHeight() @@ -149,7 +175,7 @@ public function getExtensionAttributes() * Set an extension attributes object. * * @param \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes - * @return $this + * @return void */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Model/ProductRenderList.php b/app/code/Magento/Catalog/Model/ProductRenderList.php index a3d906cf10c15..d1f60c098630e 100644 --- a/app/code/Magento/Catalog/Model/ProductRenderList.php +++ b/app/code/Magento/Catalog/Model/ProductRenderList.php @@ -17,8 +17,8 @@ /** * Provide product render information (this information should be enough for rendering product on front) - * for one or few products * + * Render information provided for one or few products */ class ProductRenderList implements ProductRenderListInterface { @@ -64,7 +64,6 @@ class ProductRenderList implements ProductRenderListInterface * @param ProductRenderSearchResultsFactory $searchResultFactory * @param ProductRenderFactory $productRenderDtoFactory * @param Config $config - * @param Product\Visibility $productVisibility * @param CollectionModifier $collectionModifier * @param array $productAttributes */ diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index d124bf5e42639..48f45d0ce9373 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -644,10 +644,11 @@ public function delete(ProductInterface $product) unset($this->instancesById[$product->getId()]); $this->resourceModel->delete($product); } catch (ValidatorException $e) { - throw new CouldNotSaveException(__($e->getMessage())); + throw new CouldNotSaveException(__($e->getMessage()), $e); } catch (\Exception $e) { throw new \Magento\Framework\Exception\StateException( - __('The "%1" product couldn\'t be removed.', $sku) + __('The "%1" product couldn\'t be removed.', $sku), + $e ); } $this->removeProductFromLocalCache($sku); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php index d4f5fdd5137c1..2896849b76cce 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php @@ -7,6 +7,7 @@ /** * Flat abstract collection + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Collection @@ -34,7 +35,7 @@ public function setSelectCountSql(\Magento\Framework\DB\Select $countSelect) } /** - * get select count sql + * Get select count sql * * @return \Magento\Framework\DB\Select */ @@ -69,6 +70,7 @@ protected function _attributeToField($attribute) /** * Add attribute to select result set. + * * Backward compatibility with EAV collection * * @param string $attribute @@ -82,6 +84,7 @@ public function addAttributeToSelect($attribute) /** * Specify collection select filter by attribute value + * * Backward compatibility with EAV collection * * @param string|\Magento\Eav\Model\Entity\Attribute $attribute @@ -96,6 +99,7 @@ public function addAttributeToFilter($attribute, $condition = null) /** * Specify collection select order by attribute value + * * Backward compatibility with EAV collection * * @param string $attribute @@ -110,6 +114,7 @@ public function addAttributeToSort($attribute, $dir = 'asc') /** * Set collection page start and records to show + * * Backward compatibility with EAV collection * * @param int $pageNum @@ -124,11 +129,12 @@ public function setPage($pageNum, $pageSize) /** * Create all ids retrieving select with limitation + * * Backward compatibility with EAV collection * * @param int $limit * @param int $offset - * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection + * @return \Magento\Framework\DB\Select */ protected function _getAllIdsSelect($limit = null, $offset = null) { @@ -144,6 +150,7 @@ protected function _getAllIdsSelect($limit = null, $offset = null) /** * Retrieve all ids for collection + * * Backward compatibility with EAV collection * * @param int $limit diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index b9e629912a5b3..3d7f863b7c0d3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -7,6 +7,10 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Catalog entity abstract model @@ -37,16 +41,18 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Factory $modelFactory * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ public function __construct( \Magento\Eav\Model\Entity\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Factory $modelFactory, - $data = [] + $data = [], + UniqueValidationInterface $uniqueValidator = null ) { $this->_storeManager = $storeManager; $this->_modelFactory = $modelFactory; - parent::__construct($context, $data); + parent::__construct($context, $data, $uniqueValidator); } /** @@ -86,16 +92,14 @@ protected function _isApplicableAttribute($object, $attribute) /** * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable * - * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend - * |\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend - * |\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance + * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance * @param string $method * @param array $args array of arguments * @return boolean */ protected function _isCallableAttributeInstance($instance, $method, $args) { - if ($instance instanceof \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend + if ($instance instanceof AbstractBackend && ($method == 'beforeSave' || $method == 'afterSave') ) { $attributeCode = $instance->getAttribute()->getAttributeCode(); @@ -112,6 +116,7 @@ protected function _isCallableAttributeInstance($instance, $method, $args) /** * Retrieve select object for loading entity attributes values + * * Join attribute store value * * @param \Magento\Framework\DataObject $object @@ -244,6 +249,7 @@ protected function _saveAttributeValue($object, $attribute, $value) /** * Check if attribute present for non default Store View. + * * Prevent "delete" query locking in a case when nothing to delete * * @param AbstractAttribute $attribute @@ -485,7 +491,7 @@ protected function _canUpdateAttribute(AbstractAttribute $attribute, $value, arr * Retrieve attribute's raw value from DB. * * @param int $entityId - * @param int|string|array $attribute atrribute's ids or codes + * @param int|string|array $attribute attribute's ids or codes * @param int|\Magento\Store\Model\Store $store * @return bool|string|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 90f55cd44bdb9..536fda7e093d3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -200,7 +200,7 @@ protected function _getTree() * delete child categories * * @param \Magento\Framework\DataObject $object - * @return $this + * @return void */ protected function _beforeDelete(\Magento\Framework\DataObject $object) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 618abda0a942d..657daca13055e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -82,7 +82,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index 9ab863cde2704..3a0d47fe573fb 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -7,6 +7,7 @@ /** * Catalog EAV collection resource abstract model + * * Implement using different stores for retrieve attribute values * * @api @@ -42,7 +43,6 @@ class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCo * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -205,10 +205,7 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) } /** - * @param \Magento\Framework\DB\Select $select - * @param string $table - * @param string $type - * @return \Magento\Framework\DB\Select + * @inheritdoc */ protected function _addLoadAttributesSelectValues($select, $table, $type) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 707ebbb2964cc..23f612582f42e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -236,6 +236,8 @@ public function afterSave() ) { $this->_indexerEavProcessor->markIndexerAsInvalid(); } + + $this->_source = null; return parent::afterSave(); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index d71ec23881982..24174391be829 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -8,6 +8,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Product entity resource model @@ -101,6 +102,7 @@ class Product extends AbstractResource * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes * @param array $data * @param TableMaintainer|null $tableMaintainer + * @param UniqueValidationInterface|null $uniqueValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -115,7 +117,8 @@ public function __construct( \Magento\Eav\Model\Entity\TypeFactory $typeFactory, \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, $data = [], - TableMaintainer $tableMaintainer = null + TableMaintainer $tableMaintainer = null, + UniqueValidationInterface $uniqueValidator = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -127,7 +130,8 @@ public function __construct( $context, $storeManager, $modelFactory, - $data + $data, + $uniqueValidator ); $this->connectionName = 'catalog'; $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); @@ -289,7 +293,7 @@ protected function _afterSave(\Magento\Framework\DataObject $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -593,7 +597,7 @@ public function countAll() } /** - * {@inheritdoc} + * @inheritdoc */ public function validate($object) { @@ -633,7 +637,7 @@ public function load($object, $entityId, $attributes = []) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @since 101.0.0 */ @@ -675,6 +679,8 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** + * Retrieve entity manager object + * * @return \Magento\Framework\EntityManager\EntityManager */ private function getEntityManager() @@ -687,6 +693,8 @@ private function getEntityManager() } /** + * Retrieve ProductWebsiteLink object + * * @deprecated 101.1.0 * @return ProductWebsiteLink */ @@ -696,6 +704,8 @@ private function getProductWebsiteLink() } /** + * Retrieve CategoryLink object + * * @deprecated 101.1.0 * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink */ @@ -710,9 +720,10 @@ private function getProductCategoryLink() /** * Extends parent method to be appropriate for product. + * * Store id is required to correctly identify attribute value we are working with. * - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ protected function getAttributeRow($entity, $object, $attribute) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index bd314c0192d38..9a030e0c37355 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -31,6 +31,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection @@ -297,6 +298,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac /** * Collection constructor + * * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -322,6 +324,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -1437,7 +1440,7 @@ protected function _addUrlRewrite() 'u.url_rewrite_id=cu.url_rewrite_id' )->where('cu.url_rewrite_id IS NULL'); } - + // more priority is data with category id $urlRewrites = []; @@ -1973,6 +1976,7 @@ protected function _productLimitationPrice($joinLeft = false) } // Set additional field filters foreach ($this->_priceDataFieldFilters as $filterData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $select->where(call_user_func_array('sprintf', $filterData)); } } else { @@ -2218,7 +2222,7 @@ private function getTierPriceSelect(array $productIds) $this->getLinkField() . ' IN(?)', $productIds )->order( - $this->getLinkField() + 'qty' ); return $select; } @@ -2278,6 +2282,7 @@ private function getBackend() public function addPriceDataFieldFilter($comparisonFormat, $fields) { if (!preg_match('/^%s( (<|>|=|<=|>=|<>) %s)*$/', $comparisonFormat)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Invalid comparison format.'); } 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 7c78dbca5a004..aa6fb8c1f8827 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 @@ -12,6 +12,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection @@ -75,7 +76,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem * @param \Magento\Catalog\Helper\Product\Compare $catalogProductCompare * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -403,6 +403,7 @@ public function clear() /** * Retrieve is flat enabled flag + * * Overwrite disable flat for compared item if required EAV resource * * @return bool diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index 635715a60742f..a9741cd8e1ec7 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Store\Model\Store; @@ -149,7 +150,7 @@ public function loadProductGalleryByAttributeId($product, $attributeId) */ protected function createBaseLoadSelect($entityId, $storeId, $attributeId) { - $select = $this->createBatchBaseSelect($storeId, $attributeId); + $select = $this->createBatchBaseSelect($storeId, $attributeId); $select = $select->where( 'entity.' . $this->metadata->getLinkField() . ' = ?', @@ -378,9 +379,9 @@ public function deleteGalleryValueInStore($valueId, $entityId, $storeId) $conditions = implode( ' AND ', [ - $this->getConnection()->quoteInto('value_id = ?', (int) $valueId), - $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int) $entityId), - $this->getConnection()->quoteInto('store_id = ?', (int) $storeId) + $this->getConnection()->quoteInto('value_id = ?', (int)$valueId), + $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int)$entityId), + $this->getConnection()->quoteInto('store_id = ?', (int)$storeId) ] ); @@ -408,7 +409,7 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu $select = $this->getConnection()->select()->from( [$this->getMainTableAlias() => $this->getMainTable()], - ['value_id', 'value'] + ['value_id', 'value', 'media_type', 'disabled'] )->joinInner( ['entity' => $this->getTable(self::GALLERY_VALUE_TO_ENTITY_TABLE)], $this->getMainTableAlias() . '.value_id = entity.value_id', @@ -425,16 +426,16 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu // Duplicate main entries of gallery foreach ($this->getConnection()->fetchAll($select) as $row) { - $data = [ - 'attribute_id' => $attributeId, - 'value' => isset($newFiles[$row['value_id']]) ? $newFiles[$row['value_id']] : $row['value'], - ]; + $data = $row; + $data['attribute_id'] = $attributeId; + $data['value'] = $newFiles[$row['value_id']] ?? $row['value']; + unset($data['value_id']); $valueIdMap[$row['value_id']] = $this->insertGallery($data); $this->bindValueToEntity($valueIdMap[$row['value_id']], $newProductId); } - if (count($valueIdMap) == 0) { + if (count($valueIdMap) === 0) { return []; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index c33ea7c781aa3..e024f0d30f1dc 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -24,13 +24,11 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ protected $_eventManager = null; /** - * AbstractEav constructor. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param null $connectionName - * @param \Magento\Indexer\Model\Indexer\StateFactory|null $stateFactory + * @param string $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -70,7 +68,6 @@ public function reindexAll() /** * Rebuild index data by entities * - * * @param int|array $processIds * @return $this * @throws \Exception @@ -88,8 +85,8 @@ public function reindexEntities($processIds) /** * Rebuild index data by attribute id - * If attribute is not indexable remove data by attribute * + * If attribute is not indexable remove data by attribute * * @param int $attributeId * @param bool $isIndexable @@ -245,7 +242,8 @@ protected function _prepareRelationIndex($parentIds = null) /** * Retrieve condition for retrieve indexable attribute select - * the catalog/eav_attribute table must have alias is ca + * + * The catalog/eav_attribute table must have alias is ca * * @return string */ diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php index 47fc6802d7eaf..463da8762b7cf 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php @@ -127,6 +127,8 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = } /** + * Check if custom options exist. + * * @param IndexTableStructure $priceTable * @return bool * @throws \Exception @@ -154,6 +156,8 @@ private function checkIfCustomOptionsExist(IndexTableStructure $priceTable): boo } /** + * Get connection. + * * @return \Magento\Framework\DB\Adapter\AdapterInterface */ private function getConnection() @@ -211,7 +215,7 @@ private function getSelectForOptionsWithMultipleValues(string $sourceTable): Sel } else { $select->joinLeft( ['otps' => $this->getTable('catalog_product_option_type_price')], - 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cwd.default_store_id', + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cwd.default_store_id', [] ); @@ -373,6 +377,8 @@ private function getSelectAggregated(string $sourceTable): Select } /** + * Get select for update. + * * @param string $sourceTable * @return \Magento\Framework\DB\Select */ @@ -402,6 +408,8 @@ private function getSelectForUpdate(string $sourceTable): Select } /** + * Get table name. + * * @param string $tableName * @return string */ @@ -411,6 +419,8 @@ private function getTable(string $tableName): string } /** + * Is price scope global. + * * @return bool */ private function isPriceGlobal(): bool diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 168fa8f50acc2..3b4c3408e742b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -10,6 +10,7 @@ /** * Default Product Type Price Indexer Resource model + * * For correctly work need define product type id * * @api @@ -208,6 +209,8 @@ public function reindexEntity($entityIds) } /** + * Reindex prices. + * * @param null|int|array $entityIds * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice */ @@ -604,7 +607,7 @@ protected function _applyCustomOption() [] )->joinLeft( ['otps' => $this->getTable('catalog_product_option_type_price')], - 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cs.store_id', + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cs.store_id', [] )->group( ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id'] @@ -802,6 +805,8 @@ public function getIdxTable($table = null) } /** + * Check if product exists. + * * @return bool */ protected function hasEntity() @@ -823,6 +828,8 @@ protected function hasEntity() } /** + * Get total tier price expression. + * * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr */ @@ -863,6 +870,8 @@ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) } /** + * Get tier price expression for table. + * * @param string $tableAlias * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index 0005ac8dea58a..95fecc832fa26 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -16,6 +16,7 @@ /** * Prepare base select for Product Price index limited by specified dimensions: website and customer group + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class BaseFinalPrice @@ -66,10 +67,11 @@ class BaseFinalPrice private $metadataPool; /** - * BaseFinalPrice constructor. * @param \Magento\Framework\App\ResourceConnection $resource * @param JoinAttributeProcessor $joinAttributeProcessor * @param \Magento\Framework\Module\Manager $moduleManager + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param string $connectionName */ public function __construct( @@ -89,6 +91,8 @@ public function __construct( } /** + * Build query for base final price. + * * @param Dimension[] $dimensions * @param string $productType * @param array $entityIds @@ -285,7 +289,7 @@ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) /** * Get tier price expression for table * - * @param $tableAlias + * @param string $tableAlias * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr */ @@ -305,7 +309,7 @@ private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $pric /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php index 54673cb01bb1d..89daab2885970 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php @@ -30,7 +30,7 @@ class TemporaryTableStrategy implements \Magento\Framework\Indexer\Table\Strateg /** * TemporaryTableStrategy constructor. - * @param \Magento\Framework\Indexer\Table\Strategy $strategy + * @param \Magento\Framework\Indexer\Table\StrategyInterface $strategy * @param \Magento\Framework\App\ResourceConnection $resource */ public function __construct( @@ -66,9 +66,10 @@ public function getTableName($tablePrefix) } /** - * Create temporary index table based on memory table + * Create temporary index table based on memory table{@inheritdoc} * - * {@inheritdoc} + * @param string $tablePrefix + * @return string */ public function prepareTableName($tablePrefix) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php index 024c87c9fc886..a554ff2641dfe 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php @@ -60,9 +60,11 @@ public function __construct( } /** + * Delete linked product. + * * @param string $entityType * @param object $entity - * @return object + * @return void * @throws CouldNotDeleteException * @throws NoSuchEntityException * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php index 8841b6059c46f..841fe17bdcf05 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php @@ -11,6 +11,9 @@ use Magento\Framework\DB\Select; use Magento\Store\Model\Store; +/** + * Provide Select object for retrieve product id with minimal price. + */ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilderInterface { /** @@ -69,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build($productId) { @@ -85,7 +88,7 @@ public function build($productId) [] )->joinInner( [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], - sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $priceAttribute->getBackendTable()], diff --git a/app/code/Magento/Catalog/Model/Rss/Category.php b/app/code/Magento/Catalog/Model/Rss/Category.php index a58569d1b59d7..653d86b177a52 100644 --- a/app/code/Magento/Catalog/Model/Rss/Category.php +++ b/app/code/Magento/Catalog/Model/Rss/Category.php @@ -6,8 +6,7 @@ namespace Magento\Catalog\Model\Rss; /** - * Class Category - * @package Magento\Catalog\Model\Rss + * Rss Category model. */ class Category { @@ -42,9 +41,11 @@ public function __construct( } /** + * Get products for given category. + * * @param \Magento\Catalog\Model\Category $category * @param int $storeId - * @return $this + * @return \Magento\Catalog\Model\ResourceModel\Product\Collection */ public function getProductCollection(\Magento\Catalog\Model\Category $category, $storeId) { diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 1eb30ff95a40b..8cd61415b958a 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -66,7 +66,7 @@ public function __construct( * Set use absolute links flag * * @param bool $flag - * @return \Magento\Email\Model\Template\Filter + * @return $this */ public function setUseAbsoluteLinks($flag) { @@ -76,10 +76,11 @@ public function setUseAbsoluteLinks($flag) /** * Setter whether SID is allowed in store directive + * * Doesn't set anything intentionally, since SID is not allowed in any kind of emails * * @param bool $flag - * @return \Magento\Email\Model\Template\Filter + * @return $this */ public function setUseSessionInUrl($flag) { @@ -132,6 +133,7 @@ public function mediaDirective($construction) /** * Retrieve store URL directive + * * Support url and direct_url properties * * @param array $construction diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php index a597b8fddda9f..ed9f89efc6891 100644 --- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php +++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Set value for Special Price start date @@ -13,21 +16,20 @@ class SetSpecialPriceStartDate implements ObserverInterface { /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @var TimezoneInterface */ private $localeDate; /** - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @codeCoverageIgnore + * @param TimezoneInterface $localeDate */ - public function __construct(\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate) + public function __construct(TimezoneInterface $localeDate) { $this->localeDate = $localeDate; } /** - * Set the current date to Special Price From attribute if it empty + * Set the current date to Special Price From attribute if it's empty. * * @param \Magento\Framework\Event\Observer $observer * @return $this @@ -36,8 +38,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); - if ($product->getSpecialPrice() && !$product->getSpecialFromDate()) { - $product->setData('special_from_date', $this->localeDate->date()); + if ($product->getSpecialPrice() && ! $product->getSpecialFromDate()) { + $product->setData('special_from_date', $this->localeDate->date()->setTime(0, 0)); } return $this; diff --git a/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php new file mode 100644 index 0000000000000..dd750cfbc696e --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Plugin\Model\Product\Option; + +/** + * Plugin for updating product 'has_options' and 'required_options' attributes + */ +class UpdateProductCustomOptionsAttributes +{ + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ + public function __construct(\Magento\Catalog\Api\ProductRepositoryInterface $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * Update product 'has_options' and 'required_options' attributes after option save + * + * @param \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject + * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + * + * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + ) { + $product = $this->productRepository->get($option->getProductSku()); + if (!$product->getHasOptions() || + ($option->getIsRequire() && !$product->getRequiredOptions())) { + $product->setCanSaveCustomOptions(true); + $product->setOptionsSaved(true); + $currentOptions = array_filter($product->getOptions(), function ($iOption) use ($option) { + return $option->getOptionId() != $iOption->getOptionId(); + }); + $currentOptions[] = $option; + $product->setOptions($currentOptions); + $product->save(); + } + + return $option; + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php index dfa06b6ebe6c8..b942f5570f57d 100644 --- a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php @@ -8,6 +8,9 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\SerializerInterface; +/** + * Config cache plugin. + */ class Config { /**#@+ @@ -46,8 +49,10 @@ public function __construct( } /** + * Cache attribute used in listing. + * * @param \Magento\Catalog\Model\ResourceModel\Config $config - * @param callable $proceed + * @param \Closure $proceed * @return array */ public function aroundGetAttributesUsedInListing( @@ -73,8 +78,10 @@ public function aroundGetAttributesUsedInListing( } /** + * Cache attributes used for sorting. + * * @param \Magento\Catalog\Model\ResourceModel\Config $config - * @param callable $proceed + * @param \Closure $proceed * @return array */ public function aroundGetAttributesUsedForSortBy( diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index 387ef9416ef68..a5e573caa381e 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -29,8 +29,10 @@ public function __construct(CalculatorInterface $calculator) } /** - * Get raw value of "as low as" as a minimal among tier prices - * {@inheritdoc} + * Get raw value of "as low as" as a minimal among tier prices{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return float|null */ public function getValue(SaleableInterface $saleableItem) { @@ -49,8 +51,10 @@ public function getValue(SaleableInterface $saleableItem) } /** - * Return calculated amount object that keeps "as low as" value - * {@inheritdoc} + * Return calculated amount object that keeps "as low as" value{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null */ public function getAmount(SaleableInterface $saleableItem) { @@ -58,6 +62,6 @@ public function getAmount(SaleableInterface $saleableItem) return $value === null ? null - : $this->calculator->getAmount($value, $saleableItem); + : $this->calculator->getAmount($value, $saleableItem, 'tax'); } } diff --git a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php index b1bfc6ff4ad6f..77c48fdb1667e 100644 --- a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php @@ -11,6 +11,7 @@ use Magento\Framework\Pricing\Price\AbstractPrice; use Magento\Framework\Pricing\Price\BasePriceProviderInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\WebsiteInterface; /** * Special price model @@ -46,6 +47,8 @@ public function __construct( } /** + * Retrieve special price. + * * @return bool|float */ public function getValue() @@ -96,19 +99,19 @@ public function getSpecialToDate() } /** - * @return bool + * @inheritdoc */ public function isScopeDateInInterval() { return $this->localeDate->isScopeDateInInterval( - $this->product->getStore(), + WebsiteInterface::ADMIN_CODE, $this->getSpecialFromDate(), $this->getSpecialToDate() ); } /** - * @return bool + * @inheritdoc */ public function isPercentageDiscount() { diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml index 692487c1d60cd..a544be434f9c5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml @@ -11,9 +11,13 @@ <arguments> <argument name="product" defaultValue="product"/> </arguments> - <amOnPage url="/{{product.name}}.html" stepKey="navigateProductPage"/> + <amOnPage url="{{StorefrontProductPage.url(product.custom_attributes[url_key])}}" stepKey="goToProductPage"/> <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> - <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> - <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddedToCartMessage"/> + <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdding}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> + <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdded}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> + <waitForElementVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAddToCart}}" stepKey="waitForElementVisibleAddToCartButtonTitleIsAddToCart"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForProductAddedMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index 57f91b78fcbe9..90d732c9654e1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -154,7 +154,7 @@ <fillField stepKey="fillCategoryName" selector="{{AdminProductCategoryCreationSection.nameInput}}" userInput="{{categoryName}}"/> - <!-- Search and select a parent catagory for the product --> + <!-- Search and select a parent category for the product --> <click stepKey="clickParentCategory" selector="{{AdminProductCategoryCreationSection.parentCategory}}"/> <waitForPageLoad stepKey="waitForDropDownVisible"/> <fillField stepKey="searchForParent" userInput="{{parentCategoryName}}" selector="{{AdminProductCategoryCreationSection.parentSearch}}"/> @@ -231,6 +231,23 @@ <waitForPageLoad stepKey="waitForStoreViewChangeLoad"/> </actionGroup> + <actionGroup name="switchCategoryToAllStoreView"> + <arguments> + <argument name="CatName"/> + </arguments> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatName)}}" stepKey="navigateToCreatedCategory" /> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForLoadingMaskToDisappear stepKey="waitForSpinner1"/> + <scrollToTopOfPage stepKey="scrollToToggle"/> + <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="openStoreViewDropDown"/> + <click selector="{{AdminCategoryMainActionsSection.allStoreViews}}" stepKey="clickStoreViewByName"/> + <see selector="{{AdminCategoryMainActionsSection.storeSwitcher}}" userInput="All Store Views" stepKey="seeAllStoreView"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <waitForLoadingMaskToDisappear stepKey="waitForSpinner2"/> + <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewModalAccept}}" stepKey="selectStoreViewAccept"/> + <waitForPageLoad stepKey="waitForStoreViewChangeLoad"/> + </actionGroup> + <actionGroup name="navigateToCreatedCategory"> <arguments> <argument name="Category"/> @@ -263,4 +280,40 @@ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> </actionGroup> + <actionGroup name="OpenCategoryFromCategoryTree"> + <arguments> + <argument name="category" type="string"/> + </arguments> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(category)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <waitForElementVisible selector="{{AdminCategoryContentSection.categoryPageTitle}}" stepKey="waitForCategoryTitle"/> + </actionGroup> + <actionGroup name="AdminAssignProductToCategory"> + <arguments> + <argument name="productId" type="string"/> + <argument name="categoryName" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="amOnPage"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="selectCategory"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveProductMessage"/> + </actionGroup> + <actionGroup name="FillCategoryNameAndUrlKeyAndSave"> + <arguments> + <argument name="categoryName" type="string"/> + <argument name="categoryUrlKey" type="string"/> + </arguments> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="enterCategoryName"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryUrlKey}}" stepKey="enterURLKey"/> + <scrollToTopOfPage stepKey="scrollToTheTopOfPage"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml new file mode 100644 index 0000000000000..dd66919640a73 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.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="AdminCreateRecentlyProductsWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <selectOption selector="{{AdminCatalogProductWidgetSection.productAttributesToShow}}" parameterArray="['Name', 'Image', 'Price']" stepKey="selectAllProductAttributes"/> + <selectOption selector="{{AdminCatalogProductWidgetSection.productButtonsToShow}}" parameterArray="['Add to Cart', 'Add to Compare', 'Add to Wishlist']" stepKey="selectAllProductButtons"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml new file mode 100644 index 0000000000000..fe859fab52667 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.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="AdminOpenNewProductFormPageActionGroup"> + <arguments> + <argument name="productType" type="string" defaultValue="simple" /> + <argument name="attributeSetId" type="string" defaultValue="{{defaultAttributeSet.attribute_set_id}}" /> + </arguments> + + <amOnPage url="{{AdminProductCreatePage.url(attributeSetId, productType)}}" stepKey="openProductNewPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 28587e6405e49..3c44a8f1898ad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -15,12 +15,20 @@ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForElementVisible selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="waitForAddProductDropdown" time="30"/> <click selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="clickAddProductType"/> - <waitForPageLoad stepKey="waitForCreateProductPageLoad"/> + <waitForPageLoad time="30" stepKey="waitForCreateProductPageLoad"/> <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, product.type_id)}}" stepKey="seeNewProductUrl"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Product" stepKey="seeNewProductTitle"/> </actionGroup> + + <!--Navigate to create product page directly via ID--> + <actionGroup name="goToProductPageViaID"> + <arguments> + <argument name="productId" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="goToProduct"/> + </actionGroup> - <!--Fill main fields in create product form--> + <!-- Fill main fields in create product form using a product entity --> <actionGroup name="fillMainProductForm"> <arguments> <argument name="product" defaultValue="_defaultProduct"/> @@ -34,6 +42,25 @@ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{product.weight}}" stepKey="fillProductWeight"/> </actionGroup> + <!-- Fill main fields in create product form using strings for flexibility --> + <actionGroup name="FillMainProductFormByString"> + <arguments> + <argument name="productName" type="string"/> + <argument name="productSku" type="string"/> + <argument name="productPrice" type="string"/> + <argument name="productQuantity" type="string"/> + <argument name="productStatus" type="string"/> + <argument name="productWeight" type="string"/> + </arguments> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{productName}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{productSku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{productPrice}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{productQuantity}}" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{productStatus}}" stepKey="selectStockStatus"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeight"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{productWeight}}" stepKey="fillProductWeight"/> + </actionGroup> + <!--Fill main fields in create product form with no weight, useful for virtual and downloadable products --> <actionGroup name="fillMainProductFormNoWeight"> <arguments> @@ -71,10 +98,20 @@ <!--Save product and see success message--> <actionGroup name="saveProductForm"> <scrollToTopOfPage stepKey="scrollTopPageProduct"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton" /> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductToSave"/> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> </actionGroup> + <actionGroup name="toggleProductEnabled"> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="toggleEnabled"/> + </actionGroup> + <!-- Save product but do not expect a success message --> + <actionGroup name="SaveProductFormNoSuccessCheck" extends="saveProductForm"> + <remove keyForRemoval="seeSaveConfirmation"/> + </actionGroup> + <!--Upload image for product--> <actionGroup name="addProductImage"> <arguments> @@ -160,7 +197,7 @@ <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> - <fillField userInput="option1" selector="{{AdminProductCustomizableOptionsSection.optionTitleInput}}" stepKey="fillOptionTitle"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="option1" stepKey="fillOptionTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openTypeDropDown"/> <click selector="{{AdminProductCustomizableOptionsSection.optionTypeTextField}}" stepKey="selectTypeTextField"/> <fillField userInput="20" selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput}}" stepKey="fillMaxChars"/> @@ -178,12 +215,14 @@ <arguments> <argument name="website" type="string"/> </arguments> - <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="ScrollToWebsites"/> - <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="ClickTpOpenProductInWebsite"/> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{ProductInWebsitesSection.website(website)}}" visible="false" stepKey="clickToOpenProductInWebsite"/> <waitForPageLoad stepKey="waitForPageOpened"/> - <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="SelectWebsite"/> + <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct"/> - <waitForPageLoad time='60' stepKey="waitForPageOpened1"/> + <waitForPageLoad time='60' stepKey="waitForProducrSaved"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSaveSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveSuccessMessage"/> </actionGroup> <actionGroup name="ProductSetAdvancedPricing"> @@ -235,10 +274,29 @@ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <checkOption selector="{{AdminProductModalSlideGridSection.productRowCheckboxBySku(sku)}}" stepKey="selectProduct"/> <click selector="{{AdminAddRelatedProductsModalSection.AddSelectedProductsButton}}" stepKey="addRelatedProductSelected"/> </actionGroup> + <!--Click AddCrossSellProducts and adds product by SKU--> + <actionGroup name="addCrossSellProductBySku"> + <arguments> + <argument name="sku"/> + </arguments> + <!--Scroll up to avoid error--> + <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" x="0" y="-100" stepKey="scrollTo"/> + <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedUpSellCrossSell"/> + <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddCrossSellProductsButton}}" stepKey="clickAddCrossSellButton"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <click selector="{{AdminProductCrossSellModalSection.addSelectedProducts}}" stepKey="addRelatedProductSelected"/> + <waitForPageLoad stepKey="waitForModalDisappear"/> + </actionGroup> + <!--Add special price to product in Admin product page--> <actionGroup name="AddSpecialPriceToProductActionGroup"> <arguments> @@ -258,11 +316,25 @@ <argument name="website" type="string"/> </arguments> <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> - <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenProductInWebsite"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandSection"/> <waitForPageLoad stepKey="waitForPageOpened"/> <checkOption selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> </actionGroup> + <actionGroup name="AdminProductAddSpecialPrice"> + <arguments> + <argument name="specialPrice" type="string"/> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitSpecialPrice1"/> + <click selector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" stepKey="checkUseDefault"/> + <fillField userInput="{{specialPrice}}" selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> + <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + <!--Switch to New Store view--> <actionGroup name="SwitchToTheNewStoreView"> <arguments> @@ -302,9 +374,7 @@ <argument name="website"/> <argument name="product"/> </arguments> - <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="navigateToProductPage"/> - <waitForPageLoad stepKey="waitForProductsList"/> - <click stepKey="openProduct" selector="{{AdminProductGridActionSection.productName(product.name)}}"/> + <click stepKey="openProduct" selector="{{AdminProductGridActionSection.productName(product.sku)}}"/> <waitForPageLoad stepKey="waitForProductPage"/> <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="ScrollToWebsites"/> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openWebsitesList"/> @@ -328,4 +398,89 @@ </assertEquals> </actionGroup> + <!-- This action group goes to the product index page, opens the drop down and clicks the specified product type for adding a product --> + <actionGroup name="GoToSpecifiedCreateProductPage"> + <arguments> + <argument type="string" name="productType" defaultValue="simple"/> + </arguments> + <comment userInput="actionGroup:GoToSpecifiedCreateProductPage" stepKey="actionGroupComment"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addTypeProduct(productType)}}" stepKey="clickAddProduct"/> + <waitForPageLoad stepKey="waitForFormToLoad"/> + </actionGroup> + + <!-- This action group simply navigates to the product catalog page --> + <actionGroup name="GoToProductCatalogPage"> + <comment userInput="actionGroup:GoToProductCatalogPage" stepKey="actionGroupComment"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> + <waitForPageLoad stepKey="WaitForPageToLoad"/> + </actionGroup> + + <actionGroup name="SetProductUrlKey"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> + + <actionGroup name="SetProductUrlKeyByString"> + <arguments> + <argument name="urlKey" type="string"/> + </arguments> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> + + <actionGroup name="SetCategoryByName"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="searchAndSelectCategory"/> + </actionGroup> + + <actionGroup name="expandAdminProductSection"> + <arguments> + <argument name="sectionSelector" defaultValue="{{AdminProductContentSection.sectionHeader}}" type="string"/> + <argument name="sectionDependentSelector" defaultValue="{{AdminProductContentSection.sectionHeaderShow}}" type="string"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <waitForElementVisible time="30" selector="{{sectionSelector}}" stepKey="waitForSection"/> + <conditionalClick selector="{{sectionSelector}}" dependentSelector="{{sectionDependentSelector}}" visible="false" stepKey="expandSection"/> + <waitForPageLoad time="30" stepKey="waitForSectionToExpand"/> + </actionGroup> + <actionGroup name="navigateToCreatedProductEditPage"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndexPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForPageLoad stepKey="waitForClearFilters"/> + <dontSeeElement selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="dontSeeClearFilters"/> + <click selector="{{AdminProductGridFilterSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> + <click selector="{{AdminProductGridFilterSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> + <waitForPageLoad stepKey="waitForResetToDefaultView"/> + <see selector="{{AdminProductGridFilterSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForFilterOnGrid"/> + <click selector="{{AdminProductGridSection.selectRowBasedOnName(product.name)}}" stepKey="clickProduct"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.productSku}}" stepKey="waitForProductSKUField"/> + <seeInField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{product.sku}}" stepKey="seeProductSKU"/> + </actionGroup> + <actionGroup name="addUpSellProductBySku" extends="addRelatedProductBySku"> + <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddUpSellProductsButton}}" stepKey="clickAddRelatedProductButton"/> + <conditionalClick selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminAddUpSellProductsModalSection.Modal}}{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <click selector="{{AdminAddUpSellProductsModalSection.AddSelectedProductsButton}}" stepKey="addRelatedProductSelected"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index 80cadbb6571f2..86158aba68f82 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -30,6 +30,95 @@ <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="navigateToAttributeEditPage3" /> <waitForPageLoad stepKey="waitForPageLoad3" /> </actionGroup> + + <actionGroup name="AdminCreateAttributeFromProductPage"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="attributeType" type="string" defaultValue="TextField"/> + </arguments> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> + <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" stepKey="selectAttributeType"/> + <click selector="{{AdminProductFormNewAttributeSection.saveAttribute}}" stepKey="saveAttribute"/> + </actionGroup> + + <actionGroup name="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" extends="AdminCreateAttributeFromProductPage"> + <remove keyForRemoval="saveAttribute"/> + <arguments> + <argument name="firstStoreViewName" type="string"/> + <argument name="secondStoreViewName" type="string"/> + </arguments> + <click selector="{{AdminProductFormNewAttributeSection.addValue}}" stepKey="addValue" after="selectAttributeType"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.optionViewName(firstStoreViewName))}}" stepKey="seeFirstStoreView"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.optionViewName(firstStoreViewName))}}" stepKey="seeSecondStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('1'))}}" userInput="default" stepKey="fillDefaultStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('2'))}}" userInput="admin" stepKey="fillAdminStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('3'))}}" userInput="view1" stepKey="fillFirstStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('4'))}}" userInput="view2" stepKey="fillSecondStoreView"/> + + <!--Check store view in Manage Titles section--> + <click selector="{{AdminProductFormNewAttributeSection.manageTitlesHeader}}" stepKey="openManageTitlesSection"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.manageTitlesViewName(customStoreEN.name)}}" stepKey="seeFirstStoreViewName"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.manageTitlesViewName(customStoreFR.name)}}" stepKey="seeSecondStoreViewName"/> + <click selector="{{AdminProductFormNewAttributeSection.saveAttribute}}" stepKey="saveAttribute1"/> + </actionGroup> + + <!-- Creates attribute and attribute set from the product page--> + <actionGroup name="AdminProductPageCreateAttributeSetWithAttribute"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="attributeSetName" type="string"/> + <argument name="attributeType" type="string" defaultValue="TextField"/> + </arguments> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForSidePanel"/> + <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> + <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" stepKey="selectAttributeType"/> + <click selector="{{AdminProductFormNewAttributeSection.saveInNewSet}}" stepKey="saveAttribute"/> + <fillField selector="{{AdminProductFormNewAttributeNewSetSection.setName}}" userInput="{{attributeSetName}}" stepKey="fillSetName"/> + <click selector="{{AdminProductFormNewAttributeNewSetSection.accept}}" stepKey="acceptPopup"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingToFinish"/> + <!-- Product page will hang if there is no reload--> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForReload"/> + </actionGroup> + + <!-- Create attribute and set with given search weight and defaultValue from the Edit Product Page --> + <actionGroup name="AdminCreateAttributeWithSearchWeight" extends="AdminProductPageCreateAttributeSetWithAttribute" insertAfter="selectAttributeType"> + <arguments> + <argument name="weight" type="string" defaultValue="1"/> + <argument name="defaultValue" type="string" defaultValue="default"/> + </arguments> + <click selector="{{AdminProductFormNewAttributeAdvancedSection.sectionHeader}}" stepKey="openAdvancedSection"/> + <fillField selector="{{AdminProductFormNewAttributeAdvancedSection.defaultValue}}" userInput="{{defaultValue}}" stepKey="inputDefault"/> + <click selector="{{AdminProductFormNewAttributeStorefrontSection.sectionHeader}}" stepKey="openStorefrontSection"/> + <checkOption selector="{{AdminProductFormNewAttributeStorefrontSection.useInSearch}}" stepKey="checkUseInSearch"/> + <waitForElementVisible selector="{{AdminProductFormNewAttributeStorefrontSection.searchWeight}}" stepKey="waitForSearchWeight"/> + <selectOption selector="{{AdminProductFormNewAttributeStorefrontSection.searchWeight}}" userInput="{{weight}}" stepKey="selectWeight"/> + </actionGroup> + + <actionGroup name="AdminProductPageSelectAttributeSet"> + <arguments> + <argument name="attributeSetName" type="string"/> + </arguments> + <click stepKey="openDropdown" selector="{{AdminProductFormSection.attributeSet}}"/> + <fillField stepKey="filter" selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSetName}}"/> + <click stepKey="clickResult" selector="{{AdminProductFormSection.attributeSetFilterResult}}"/> + </actionGroup> + + <actionGroup name="AdminProductPageFillTextAttributeValueByName"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="value" type="string"/> + </arguments> + <click stepKey="openSection" selector="{{AdminProductAttributeSection.attributeSectionHeader}}"/> + <fillField stepKey="fillValue" selector="{{AdminProductAttributeSection.textAttributeByName(attributeName)}}" userInput="{{value}}"/> + </actionGroup> + <actionGroup name="changeUseForPromoRuleConditionsProductAttribute"> <arguments> <argument name="option" type="string"/> @@ -47,4 +136,132 @@ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> </actionGroup> + <actionGroup name="deleteProductAttributeByLabel"> + <arguments> + <argument name="ProductAttribute"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttribute.default_label}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> + <!-- Delete product attribute by Attribute Code --> + <actionGroup name="deleteProductAttributeByAttributeCode"> + <arguments> + <argument name="ProductAttributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForPageLoad2" /> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> + <!--Filter product attribute by Attribute Code --> + <actionGroup name="filterProductAttributeByAttributeCode"> + <arguments> + <argument name="ProductAttributeCode" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + </actionGroup> + <!--Filter product attribute by Default Label --> + <actionGroup name="filterProductAttributeByDefaultLabel"> + <arguments> + <argument name="productAttributeLabel" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="{{productAttributeLabel}}" stepKey="setDefaultLabel"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + </actionGroup> + <actionGroup name="saveProductAttribute"> + <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="confirmChangeInputTypeModal"> + <waitForElementVisible selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostConfirmButton}}" stepKey="waitForChangeInputTypeButton"/> + <click selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostConfirmButton}}" stepKey="clickChangeInputTypeButton"/> + <waitForElementNotVisible selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostModal}}" stepKey="waitForChangeInputTypeModalGone"/> + </actionGroup> + <actionGroup name="saveProductAttributeInUse"> + <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!--Clicks Add Attribute and adds the given attribute--> + <actionGroup name="addProductAttributeInProductModal"> + <arguments> + <argument name="attributeCode" type="string"/> + </arguments> + <click stepKey="addAttribute" selector="{{AdminProductFormActionSection.addAttributeButton}}"/> + <conditionalClick selector="{{AdminProductAddAttributeModalSection.clearFilters}}" dependentSelector="{{AdminProductAddAttributeModalSection.clearFilters}}" visible="true" stepKey="clearFilters"/> + <click stepKey="clickFilters" selector="{{AdminProductAddAttributeModalSection.filters}}"/> + <fillField stepKey="fillCode" selector="{{AdminProductAddAttributeModalSection.attributeCodeFilter}}" userInput="{{attributeCode}}"/> + <click stepKey="clickApply" selector="{{AdminProductAddAttributeModalSection.applyFilters}}"/> + <waitForPageLoad stepKey="waitForFilters"/> + <checkOption selector="{{AdminProductAddAttributeModalSection.firstRowCheckBox}}" stepKey="checkAttribute"/> + <click stepKey="addSelected" selector="{{AdminProductAddAttributeModalSection.addSelected}}"/> + </actionGroup> + + <!--Clicks createNewAttribute and fills out form--> + <actionGroup name="createProductAttribute"> + <arguments> + <argument name="attribute" type="entity" defaultValue="productAttributeWysiwyg"/> + </arguments> + <click stepKey="createNewAttribute" selector="{{AdminProductAttributeGridSection.createNewAttributeBtn}}"/> + <fillField stepKey="fillDefaultLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attribute.attribute_code}}"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" stepKey="checkInputType" userInput="{{attribute.frontend_input}}"/> + <selectOption selector="{{AttributePropertiesSection.ValueRequired}}" stepKey="checkRequired" userInput="{{attribute.is_required_admin}}"/> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + </actionGroup> + + <!-- Inputs text default value and attribute code--> + <actionGroup name="createProductAttributeWithTextField" extends="createProductAttribute" insertAfter="checkRequired"> + <click stepKey="openAdvancedProperties" selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}"/> + <fillField stepKey="fillCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attribute.attribute_code}}"/> + <fillField stepKey="fillDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueText}}" userInput="{{attribute.default_value}}"/> + </actionGroup> + + <!-- Inputs date default value and attribute code--> + <actionGroup name="createProductAttributeWithDateField" extends="createProductAttribute" insertAfter="checkRequired"> + <arguments> + <argument name="date" type="string"/> + </arguments> + <click stepKey="openAdvancedProperties" selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}"/> + <fillField stepKey="fillCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attribute.attribute_code}}"/> + <fillField stepKey="fillDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueDate}}" userInput="{{date}}"/> + </actionGroup> + + <!-- Creates dropdown option at row without saving--> + <actionGroup name="createAttributeDropdownNthOption"> + <arguments> + <argument name="row" type="string"/> + <argument name="adminName" type="string"/> + <argument name="frontName" type="string"/> + </arguments> + <click stepKey="clickAddOptions" selector="{{AttributePropertiesSection.dropdownAddOptions}}"/> + <waitForPageLoad stepKey="waitForNewOption"/> + <fillField stepKey="fillAdmin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin(row)}}" userInput="{{adminName}}"/> + <fillField stepKey="fillStoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView(row)}}" userInput="{{frontName}}"/> + </actionGroup> + + <!-- Creates dropdown option at row as default--> + <actionGroup name="createAttributeDropdownNthOptionAsDefault" extends="createAttributeDropdownNthOption"> + <checkOption selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault(row)}}" stepKey="setAsDefault" after="fillStoreView"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml index 5948ca12dcf0f..019d103a31cf2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -45,4 +45,53 @@ <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{label}}" stepKey="fillName"/> <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickSave1"/> </actionGroup> + <actionGroup name="goToAttributeGridPage"> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + </actionGroup> + <actionGroup name="goToAttributeSetByName"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickResetButton"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{name}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <!-- Filter By Attribute Label --> + <actionGroup name="filterProductAttributeByAttributeLabel"> + <arguments> + <argument name="productAttributeLabel" type="string"/> + </arguments> + <fillField selector="{{AdminProductAttributeGridSection.attributeLabelFilter}}" userInput="{{productAttributeLabel}}" stepKey="setAttributeLabel"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + </actionGroup> + <actionGroup name="FilterProductAttributeSetGridByAttributeSetName"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickResetButton"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{name}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup name="deleteAttributeSetByLabel"> + <arguments> + <argument name="label" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{label}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForClick"/> + <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> + <click selector="{{AdminProductAttributeSetSection.modalOk}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="The attribute set has been removed." stepKey="seeDeleteMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index 817da18f5f2b7..ad32b8edbd243 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -144,6 +144,18 @@ <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> </actionGroup> + <!-- Filter product grid by sku, name --> + <actionGroup name="filterProductGridBySkuAndName"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + </actionGroup> + <!--Delete a product by filtering grid and using delete action--> <actionGroup name="deleteProductUsingProductGrid"> <arguments> @@ -155,6 +167,7 @@ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> @@ -179,7 +192,7 @@ </arguments> <!--TODO use other action group for filtering grid when MQE-539 is implemented --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> @@ -188,8 +201,9 @@ <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> - <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> + <see selector="{{AdminMessagesSection.success}}" userInput="record(s) have been deleted." stepKey="seeSuccessMessage"/> </actionGroup> <actionGroup name="deleteProductByName" extends="deleteProductBySku"> @@ -248,4 +262,40 @@ <waitForLoadingMaskToDisappear stepKey="waitForMaskToDisappear"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> </actionGroup> + + <actionGroup name="NavigateToAndResetProductGridToDefaultView" extends="resetProductGridToDefaultView"> + <amOnPage url="{{AdminProductIndexPage.url}}" before="clickClearFilters" stepKey="goToAdminProductIndexPage"/> + <waitForPageLoad after="goToAdminProductIndexPage" stepKey="waitForProductIndexPageToLoad"/> + </actionGroup> + <actionGroup name="NavigateToAndResetProductAttributeGridToDefaultView"> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForPageLoad stepKey="waitForGridLoad"/> + </actionGroup> + <!--Filter and select the the product --> + <actionGroup name="filterAndSelectProduct"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{productSku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku(productSku)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForProductTitle"/> + </actionGroup> + + <actionGroup name="deleteProductsIfTheyExist"> + <conditionalClick selector="{{AdminProductGridSection.multicheckDropdown}}" dependentSelector="{{AdminProductGridSection.firstProductRow}}" visible="true" stepKey="openMulticheckDropdown"/> + <conditionalClick selector="{{AdminProductGridSection.multicheckOption('Select All')}}" dependentSelector="{{AdminProductGridSection.firstProductRow}}" visible="true" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="waitForModalPopUp"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml new file mode 100644 index 0000000000000..9402ac05d79a5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.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="AssertAttributeDeletionErrorMessageActionGroup"> + <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForErrorMessage"/> + <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="This attribute is used in configurable products." stepKey="deleteProductAttributeFailureMessage"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.xml new file mode 100644 index 0000000000000..25390e6b4a80b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.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="AssertProductAttributePresenceInCatalogProductGridActionGroup"> + <arguments> + <argument name="productAttribute" type="entity"/> + </arguments> + <waitForPageLoad stepKey="waitForCatalogProductGridPageLoad"/> + <seeElement selector="{{AdminGridHeaders.headerByName(productAttribute.label)}}" stepKey="seeAttributeInHeaders"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml new file mode 100644 index 0000000000000..7917fe68aaebc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.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="AssertProductInfoOnEditPageActionGroup" extends="OpenEditProductOnBackendActionGroup"> + <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"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml new file mode 100644 index 0000000000000..963c9d9f1c911 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.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="AssertProductOnAdminGridActionGroup" extends="viewProductInAdminGrid"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <remove keyForRemoval="clickClearFiltersAfter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml new file mode 100644 index 0000000000000..f7cd2e7076288 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.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="CompareTwoProductsOrder"> + <arguments> + <argument name="product_1"/> + <argument name="product_2"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('1')}}" userInput="alt" stepKey="grabFirstProductName1_1"/> + <assertEquals expected="{{product_1.name}}" actual="($grabFirstProductName1_1)" message="notExpectedOrder" stepKey="compare1"/> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('2')}}" userInput="alt" stepKey="grabFirstProductName2_2"/> + <assertEquals expected="{{product_2.name}}" actual="($grabFirstProductName2_2)" message="notExpectedOrder" stepKey="compare2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml similarity index 86% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml rename to app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml index 19de3e859ae9a..53de47f810600 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml @@ -5,10 +5,10 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CreateNewProductActionGroup"> - <click stepKey="openCatalog" selector="{{AdminMenuSection.catalog}}"/> <waitForPageLoad stepKey="waitForCatalogSubmenu" time="5"/> <click stepKey="clickOnProducts" selector="{{CatalogSubmenuSection.products}}"/> @@ -22,4 +22,4 @@ <waitForElementVisible stepKey="waitForSuccessfullyCreatedMessage" selector="{{NewProductPageSection.createdSuccessMessage}}" time="10"/> <waitForPageLoad stepKey="waitForPageLoad" time="10"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml index 7373d5baea0c5..b914d5e20712d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml @@ -15,7 +15,6 @@ <argument name="productOption"/> <argument name="productOption2"/> </arguments> - <click stepKey="clickAddOptions" selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}"/> <waitForPageLoad stepKey="waitForAddProductPageLoad"/> @@ -48,10 +47,56 @@ <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> <click selector="{{AdminProductCustomizableOptionsSection.optionType('File')}}" stepKey="selectTypeFile"/> - <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" stepKey="waitForElements"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" userInput="{{option.price}}" stepKey="fillPrice"/> - <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" stepKey="waitForElements"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{option.price}}" stepKey="fillPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions('0')}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> + </actionGroup> + <actionGroup name="AddProductCustomOptionField"> + <arguments> + <argument name="option" defaultValue="ProductOptionField"/> + <argiment name="optionIndex" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" stepKey="waitForOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionType('Field')}}" stepKey="selectTypeFile"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" stepKey="waitForElements"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" userInput="{{option.price}}" stepKey="fillPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType(optionIndex)}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku(optionIndex)}}" userInput="{{option.title}}" stepKey="fillSku"/> + </actionGroup> + <actionGroup name="importProductCustomizableOptions"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> + <waitForElementVisible selector="{{AdminProductImportOptionsSection.selectProductTitle}}" stepKey="waitForTitleVisible"/> + <conditionalClick selector="{{AdminProductImportOptionsSection.resetFiltersButton}}" dependentSelector="{{AdminProductImportOptionsSection.resetFiltersButton}}" visible="true" stepKey="clickResetFilters"/> + <click selector="{{AdminProductImportOptionsSection.filterButton}}" stepKey="clickFilterButton"/> + <waitForElementVisible selector="{{AdminProductImportOptionsSection.nameField}}" stepKey="waitForNameField"/> + <fillField selector="{{AdminProductImportOptionsSection.nameField}}" userInput="{{productName}}" stepKey="fillProductName"/> + <click selector="{{AdminProductImportOptionsSection.applyFiltersButton}}" stepKey="clickApplyFilters"/> + <checkOption selector="{{AdminProductImportOptionsSection.firstRowItemCheckbox}}" stepKey="checkProductCheckbox"/> + <click selector="{{AdminProductImportOptionsSection.importButton}}" stepKey="clickImport"/> + </actionGroup> + <actionGroup name="resetImportOptionFilter"> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> + <click selector="{{AdminProductImportOptionsSection.resetFiltersButton}}" stepKey="clickResetFilterButton"/> + </actionGroup> + <actionGroup name="checkCustomizableOptionImport"> + <arguments> + <argument name="option" defaultValue="ProductOptionField"/> + <argiment name="optionIndex" type="string"/> + </arguments> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionTitleInput(optionIndex)}}" stepKey="grabOptionTitle"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" stepKey="grabOptionPrice"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionSku(optionIndex)}}" stepKey="grabOptionSku"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionTitle" stepKey="assertOptionTitle"/> + <assertEquals expected="{{option.price}}" expectedType="string" actual="$grabOptionPrice" stepKey="assertOptionPrice"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionSku" stepKey="assertOptionSku"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml similarity index 85% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml rename to app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml index 724c6d92846c4..7491b39aa8f20 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="DeleteProductActionGroup"> <arguments> <argument name="productName" defaultValue=""/> @@ -23,4 +24,4 @@ <click stepKey="clickOnOk" selector="{{ProductsPageSection.ok}}"/> <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{ProductsPageSection.deletedSuccessMessage}}" time="10"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml new file mode 100644 index 0000000000000..575cbdcd9b6dc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.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="DeleteProductAttributeByAttributeCodeActionGroup"> + <arguments> + <argument name="productAttributeCode" type="string"/> + </arguments> + <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad" time="30" /> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="clickOnConfirmOk"/> + <waitForPageLoad stepKey="waitForViewProductAttributePageLoad"/> +</actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml new file mode 100644 index 0000000000000..31b024c82a9a0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.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="OpenProductAttributeFromSearchResultInGridActionGroup" extends="SearchAttributeByCodeOnProductAttributeGridActionGroup"> + <arguments> + <argument name="productAttributeCode" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" stepKey="waitForAdminProductAttributeGridLoad"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode(productAttributeCode)}}" stepKey="clickAttributeToView"/> + <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml new file mode 100644 index 0000000000000..67cdd8192fcb4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml @@ -0,0 +1,24 @@ +<?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="SearchAttributeByCodeOnProductAttributeGridActionGroup"> + <arguments> + <argument name="productAttributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAdminProductAttributeGridLoad"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <waitForPageLoad stepKey="waitForAdminProductAttributeGridSectionLoad"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{productAttributeCode}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskOnGridToDisappear"/> + <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{productAttributeCode}}" stepKey="seeAttributeCodeInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml index a303511ffe5bb..aca9ba24c1168 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml @@ -19,6 +19,14 @@ <click selector="{{AdminProductFiltersSection.apply}}" stepKey="clickApplyFiltersButton"/> </actionGroup> + <actionGroup name="SearchForProductOnBackendByNameActionGroup" extends="SearchForProductOnBackendActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <remove keyForRemoval="fillSkuFieldOnFiltersSection"/> + <fillField userInput="{{productName}}" selector="{{AdminProductFiltersSection.nameInput}}" after="cleanFiltersIfTheySet" stepKey="fillNameFieldOnFiltersSection"/> + </actionGroup> + <actionGroup name="ClearProductsFilterActionGroup"> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..1bb7c179dfca8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.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="StorefrontAssertProductImagesOnProductPageActionGroup"> + <arguments> + <argument name="productImage" type="string" defaultValue="Magento_Catalog/images/product/placeholder/image.jpg" /> + </arguments> + <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> + <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> + <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> + <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> + <waitForPageLoad stepKey="waitForGalleryLoaded" /> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> + <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> + <waitForPageLoad stepKey="waitForGalleryDisappear" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml new file mode 100644 index 0000000000000..c25b73bab21ae --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml @@ -0,0 +1,37 @@ +<?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"> + <!-- Check the product in recently viewed widget --> + <actionGroup name="StorefrontAssertProductInRecentlyViewedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" stepKey="waitWidgetRecentlyViewedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyViewedWidget"/> + </actionGroup> + + <!-- Check the product in recently compared widget --> + <actionGroup name="StorefrontAssertProductInRecentlyComparedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" stepKey="waitWidgetRecentlyComparedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyComparedWidget"/> + </actionGroup> + + <!-- Check the product in recently ordered widget --> + <actionGroup name="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" stepKey="waitWidgetRecentlyOrderedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyOrderedWidget"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..6cb156723b286 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontAssertProductNameOnProductPageActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}" stepKey="seeProductName" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..3c62ef89e584b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontAssertProductPriceOnProductPageActionGroup"> + <arguments> + <argument name="productPrice" type="string"/> + </arguments> + <see selector="{{StorefrontProductInfoMainSection.price}}" userInput="{{productPrice}}" stepKey="seeProductPrice" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..85d3927a6d6d0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontAssertProductSkuOnProductPageActionGroup"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{productSku}}" stepKey="seeProductSku" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..fb2065d228d5a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml @@ -0,0 +1,14 @@ +<?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="StorefrontClickAddToCartOnProductPageActionGroup"> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart" /> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml new file mode 100644 index 0000000000000..f5fabae5fc4ce --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.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="StorefrontOpenProductPageActionGroup"> + <arguments> + <argument name="productUrl" type="string"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url(productUrl)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 0000000000000..aec21f3bc48c9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.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="VerifyProductTypeOrder"> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <waitForPageLoad stepKey="waitForLoad"/> + <seeElement stepKey="seeSimpleInOrder" selector="{{AdminProductDropdownOrderSection.simpleProduct}}"/> + <seeElement stepKey="seeVirtualInOrder" selector="{{AdminProductDropdownOrderSection.virtualProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..24e1fe9cf5ecd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,36 @@ +<?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="AdminMenuCatalog"> + <data key="pageTitle">Catalog</data> + <data key="title">Catalog</data> + <data key="dataUiId">magento-catalog-catalog</data> + </entity> + <entity name="AdminMenuCatalogCategories"> + <data key="pageTitle">Default Category (ID: 2)</data> + <data key="title">Categories</data> + <data key="dataUiId">magento-catalog-catalog-categories</data> + </entity> + <entity name="AdminMenuCatalogProducts"> + <data key="pageTitle">Products</data> + <data key="title">Products</data> + <data key="dataUiId">magento-catalog-catalog-products</data> + </entity> + <entity name="AdminMenuStoresAttributesAttributeSet"> + <data key="pageTitle">Attribute Sets</data> + <data key="title">Attribute Set</data> + <data key="dataUiId">magento-catalog-catalog-attributes-sets</data> + </entity> + <entity name="AdminMenuStoresAttributesProduct"> + <data key="pageTitle">Product Attributes</data> + <data key="title">Product</data> + <data key="dataUiId">magento-catalog-catalog-attributes-attributes</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml new file mode 100644 index 0000000000000..6e1b25fb9cdc4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.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="defaultAttributeSet"> + <data key="attribute_set_id">4</data> + <data key="attribute_set_name">Default</data> + <data key="sort_order">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml index cad8a8cd03e0d..0f7f4da1b68c0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="CatalogPriceScopeWebsite" type="catalog_price_config_state"> <requiredEntity type="scope">scopeWebsite</requiredEntity> <requiredEntity type="default_product_price">defaultProductPrice</requiredEntity> @@ -29,5 +29,4 @@ <entity name="defaultProductPrice" type="default_product_price"> <data key="value"/> </entity> - </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml new file mode 100644 index 0000000000000..d1e469deaebba --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml @@ -0,0 +1,25 @@ +<?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="EnableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> + <requiredEntity type="synchronize_with_backend">EnableCatalogRecentlyProductsSynchronize</requiredEntity> + </entity> + + <entity name="EnableCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> + <data key="value">1</data> + </entity> + + <entity name="DisableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> + <requiredEntity type="synchronize_with_backend">DefaultCatalogRecentlyProductsSynchronize</requiredEntity> + </entity> + + <entity name="DefaultCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml new file mode 100644 index 0000000000000..31783526932b6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="specialProductPrice" type="catalogSpecialPrice"> + <data key="price">99.99</data> + <data key="store_id">0</data> + <var key="sku" entityType="product2" entityKey="sku" /> + </entity> + <entity name="specialProductPrice2" type="catalogSpecialPrice"> + <data key="price">55.55</data> + <data key="store_id">0</data> + <var key="sku" entityType="product" entityKey="sku" /> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml index d8ec84013d93b..abf01f00dbbcc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml @@ -38,4 +38,26 @@ <entity name="DefaultFlatCatalogProduct" type="flat_catalog_product"> <data key="value">0</data> </entity> + + <entity name="UseFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">UseFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">UseFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="UseFlatCatalogProduct" type="flat_catalog_product"> + <data key="value">1</data> + </entity> + + <entity name="UseFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">1</data> + </entity> + + <entity name="DefaultFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">DefaultFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">DefaultFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="DefaultFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index ff3c5cb4403bf..27167d03d528e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -44,11 +44,11 @@ </entity> <entity name="FirstLevelSubCat" type="category"> <data key="name" unique="suffix">FirstLevelSubCategory</data> - <data key="name_lwr" unique="suffix">subcategory</data> + <data key="name_lwr" unique="suffix">firstlevelsubcategory</data> </entity> <entity name="SecondLevelSubCat" type="category"> <data key="name" unique="suffix">SecondLevelSubCategory</data> - <data key="name_lwr" unique="suffix">subcategory</data> + <data key="name_lwr" unique="suffix">secondlevelsubcategory</data> </entity> <entity name="ThirdLevelSubCat" type="category"> <data key="name" unique="suffix">ThirdLevelSubCategory</data> @@ -62,4 +62,52 @@ <data key="name" unique="suffix">FifthLevelCategory</data> <data key="name_lwr" unique="suffix">category</data> </entity> + <entity name="SimpleRootSubCategory" type="category"> + <data key="name" unique="suffix">SimpleRootSubCategory</data> + <data key="name_lwr" unique="suffix">simplerootsubcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <data key="url_key" unique="suffix">simplerootsubcategory</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="SubCategory" type="category"> + <data key="name" unique="suffix">SubCategory</data> + <data key="name_lwr" unique="suffix">subcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + </entity> + <entity name="Two_nested_categories" type="category"> + <data key="name" unique="suffix">SecondLevel</data> + <data key="url_key" unique="suffix">secondlevel</data> + <data key="name_lwr" unique="suffix">secondlevel</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="Three_nested_categories" type="category"> + <data key="name" unique="suffix">ThirdLevel</data> + <data key="url_key" unique="suffix">thirdlevel</data> + <data key="name_lwr" unique="suffix">thirdlevel</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="CatNotIncludeInMenu" type="category"> + <data key="name" unique="suffix">NotInclMenu</data> + <data key="name_lwr" unique="suffix">notinclemenu</data> + <data key="is_active">true</data> + <data key="include_in_menu">false</data> + </entity> + <entity name="CatNotActive" type="category"> + <data key="name" unique="suffix">NotActive</data> + <data key="name_lwr" unique="suffix">notactive</data> + <data key="is_active">false</data> + <data key="include_in_menu">true</data> + </entity> + <entity name="CatInactiveNotInMenu" type="category"> + <data key="name" unique="suffix">InactiveNotInMenu</data> + <data key="name_lwr" unique="suffix">inactivenotinmenu</data> + <data key="is_active">false</data> + <data key="include_in_menu">false</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml index 8a26b6babdbbc..d09880f14afbf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml @@ -13,4 +13,8 @@ <data key="one">1</data> <data key="two">2</data> </entity> + <entity name="prodNameWithSpecChars"> + <data key="trademark">"Pursuit Lumaflex™ Tone Band"</data> + <data key="skumark">"x™"</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml new file mode 100644 index 0000000000000..4479805cb12fb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.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="NewProductData" type="braintree_config_state"> + <data key="ProductName">ProductTest</data> + <data key="Price">100</data> + <data key="Quantity">100</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index b367cdcab9d8b..817dd637f81dd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -52,6 +52,41 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeWithTwoOptionsNotVisible" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">test_attr_</data> + <data key="frontend_input">select</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">false</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">false</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="hiddenDropdownAttributeWithOptions" extends="productAttributeWithTwoOptions"> + <data key="is_searchable">false</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">false</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">false</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + </entity> <entity name="productDropDownAttribute" type="ProductAttribute"> <data key="attribute_code" unique="suffix">attribute</data> <data key="frontend_input">select</data> @@ -73,6 +108,27 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productDropDownAttributeNotSearchable" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">select</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> <entity name="productAttributeWithDropdownTwoOptions" type="ProductAttribute"> <data key="attribute_code">testattribute</data> <data key="frontend_input">select</data> @@ -115,4 +171,153 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeMultiselectTwoOptionsNotSearchable" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">multiselect</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="newsFromDate" type="ProductAttribute"> + <data key="attribute_code">news_from_date</data> + <data key="default_frontend_label">Set Product as New from Date</data> + <data key="frontend_input">date</data> + <data key="is_required">false</data> + <data key="is_user_defined">true</data> + <data key="scope">website</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">false</data> + <data key="is_visible_on_front">false</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">false</data> + <data key="is_used_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="newProductAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">text</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="productYesNoAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">boolean</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="productAttributeText" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">text</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="backend_type">text</data> + <data key="is_wysiwyg_enabled">false</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="textProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">text</data> + <data key="default_value" unique="suffix">defaultValue</data> + <data key="is_required_admin">No</data> + </entity> + <entity name="dateProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">date</data> + <data key="is_required_admin">No</data> + </entity> + <entity name="priceProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">date</data> + <data key="is_required_admin">No</data> + </entity> + <entity name="dropdownProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">select</data> + <data key="frontend_input_admin">Dropdown</data> + <data key="is_required_admin">No</data> + <data key="option1_admin" unique="suffix">opt1Admin</data> + <data key="option1_frontend" unique="suffix">opt1Front</data> + <data key="option2_admin" unique="suffix">opt2Admin</data> + <data key="option2_frontend" unique="suffix">opt2Front</data> + <data key="option3_admin" unique="suffix">opt3Admin</data> + <data key="option3_frontend" unique="suffix">opt3Front</data> + </entity> + <entity name="multiselectProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">multiselect</data> + <data key="frontend_input_admin">Multiple Select</data> + <data key="is_required_admin">No</data> + <data key="option1_admin" unique="suffix">opt1Admin</data> + <data key="option1_frontend" unique="suffix">opt1Front</data> + <data key="option2_admin" unique="suffix">opt2Admin</data> + <data key="option2_frontend" unique="suffix">opt2Front</data> + <data key="option3_admin" unique="suffix">opt3Admin</data> + <data key="option3_frontend" unique="suffix">opt3Front</data> + </entity> + <entity name="dropdownProductAttributeWithQuote" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">select</data> + <data key="frontend_input_admin">Dropdown</data> + <data key="is_required_admin">No</data> + <data key="option1_admin" unique="suffix">opt1'Admin</data> + <data key="option1_frontend" unique="suffix">opt1'Front</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index 5be2a84f54555..fcb56cf298a98 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -81,4 +81,9 @@ <requiredEntity type="StoreLabel">Option9Store0</requiredEntity> <requiredEntity type="StoreLabel">Option10Store1</requiredEntity> </entity> + <entity name="ProductAttributeOption8" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">White</data> + <data key="value" unique="suffix">white</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml index 68f51559a9f31..6d4314a6d865f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml @@ -14,4 +14,16 @@ <data key="attributeGroupId">7</data> <data key="sortOrder">0</data> </entity> + <entity name="AddToDefaultSetSortOrder1" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">4</data> + <data key="attributeGroupId">7</data> + <data key="sortOrder">1</data> + </entity> + <entity name="AddToSetBlank" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">0</data> + <data key="attributeGroupId">0</data> + <data key="sortOrder">0</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 74492ed416206..b8d6ec8e63e71 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -35,6 +35,13 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="ApiSimpleProductWithSpecCharInName" type="product" extends="ApiSimpleProduct"> + <data key="name">Pursuit Lumaflex&trade; Tone Band</data> + <data key="sku" unique="suffix">x&trade;</data> + </entity> + <entity name="ApiSimpleProductWithCustomPrice" type="product" extends="ApiSimpleProduct"> + <data key="price">100</data> + </entity> <entity name="ApiSimpleProductUpdateDescription" type="product2"> <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> @@ -57,6 +64,48 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleProductAfterImport1" type="product"> + <data key="sku">SimpleProductForTest1</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name">SimpleProductAfterImport1</data> + <data key="price">250.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="urlKey">simple-product-for-test-1</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProductAfterImport2" type="product"> + <data key="sku">SimpleProductForTest2</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name">SimpleProductAfterImport2</data> + <data key="price">300.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="urlKey">simple-product-for-test-2</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProductAfterImport3" type="product"> + <data key="sku">SimpleProductForTest3</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name">SimpleProductAfterImport3</data> + <data key="price">350.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="urlKey">simple-product-for-test-3</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="SimpleProduct2" type="product"> <data key="sku" unique="suffix">SimpleProduct</data> <data key="type_id">simple</data> @@ -93,6 +142,31 @@ <data key="quantity">0</data> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleOutOfStockProduct" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">OutOfStockProduct</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="quantity">0</data> + </entity> + <!-- Simple Product Disabled --> + <entity name="SimpleProductOffline" type="product2"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">SimpleOffline</data> + <data key="price">123.00</data> + <data key="status">2</data> + <data key="quantity">100</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> <entity name="NewSimpleProduct" type="product"> <data key="price">321.00</data> </entity> @@ -107,6 +181,18 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleOutOfStock" type="product2"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Out Of Stock Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> <entity name="ApiSimpleOne" type="product2"> <data key="sku" unique="suffix">api-simple-product</data> <data key="type_id">simple</data> @@ -146,6 +232,15 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleProductWithPrice50" type="product2" extends="ApiSimpleOne"> + <data key="price">50</data> + </entity> + <entity name="ApiSimpleProductWithPrice60" type="product2" extends="ApiSimpleTwo"> + <data key="price">60</data> + </entity> + <entity name="ApiSimpleProductWithPrice70" type="product2" extends="SimpleOne"> + <data key="price">70</data> + </entity> <entity name="ApiSimpleTwoHidden" type="product2"> <data key="sku" unique="suffix">api-simple-product-two</data> <data key="type_id">simple</data> @@ -210,6 +305,15 @@ <data key="filename">magento-again</data> <data key="file_extension">jpg</data> </entity> + <entity name="TestImageAdobe" type="image"> + <data key="title" unique="suffix">magento-adobe</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">adobe-base.jpg</data> + <data key="filename">adobe-base</data> + <data key="file_extension">jpg</data> + </entity> <entity name="ProductWithUnicode" type="product"> <data key="sku" unique="suffix">霁产品</data> <data key="type_id">simple</data> @@ -260,7 +364,7 @@ <data key="status">1</data> <data key="quantity">100</data> <data key="weight">0</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> <entity name="productWithDescription" type="product"> @@ -337,6 +441,11 @@ <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> </entity> + <entity name="ProductWithTextFieldAndAreaOptions" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionField</requiredEntity> + <requiredEntity type="product_option">ProductOptionArea</requiredEntity> + </entity> <entity name="ApiVirtualProductWithDescription" type="product"> <data key="sku" unique="suffix">api-virtual-product</data> <data key="type_id">virtual</data> @@ -480,8 +589,113 @@ <requiredEntity type="product_extension_attribute">EavStock1</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="virtualProductWithRequiredFields" type="product"> + <data key="name" unique="suffix">virtualProduct</data> + <data key="sku" unique="suffix">virtualsku</data> + <data key="price">10</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtualproduct</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductBigQty" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">100.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductGeneralGroup" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">100.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductCustomImportOptions" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">9,000.00</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductWithoutManageStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">100.00</data> + <data key="quantity">999</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="special_price">90.00</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductOutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">9,000.00</data> + <data key="quantity">999</data> + <data key="status">Out of Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductAssignToCategory" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">10.00</data> + <data key="quantity">999</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductRegularPriceInStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">120.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductWithTierPriceInStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductRegularPrice99OutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> <entity name="defaultSimpleProduct" type="product"> - <data key="name" unique="suffix">Test </data> + <data key="name" unique="suffix">Testp</data> <data key="sku" unique="suffix">testsku</data> <data key="type_id">simple</data> <data key="attribute_set_id">4</data> @@ -497,6 +711,19 @@ <data key="name" unique="suffix">Product With Long Name And Sku - But not too long</data> <data key="sku" unique="suffix">Product With Long Name And Sku - But not too long</data> </entity> + <entity name="PaginationProduct" type="product"> + <data key="name" unique="suffix">pagi</data> + <data key="sku" unique="suffix">pagisku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="price">780.00</data> + <data key="urlKey" unique="suffix">pagiurl-</data> + <data key="status">1</data> + <data key="quantity">50</data> + <data key="weight">5</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> <entity name="Magento3" type="image"> <data key="title" unique="suffix">Magento3</data> <data key="price">1.00</data> @@ -506,4 +733,266 @@ <data key="filename">magento3</data> <data key="file_extension">jpg</data> </entity> + <entity name="updateVirtualProductRegularPrice" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductRegularPrice5OutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">5.00</data> + <data key="productTaxClass">None</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Catalog</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductSpecialPrice" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">120.00</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="special_price">45.00</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductSpecialPriceOutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">None</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="special_price">45.00</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductTierPriceInStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">145.00</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualTierPriceOutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">185.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="simpleProductRegularPrice325InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.02</data> + <data key="quantity">89</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">89.0000</data> + <data key="visibility">Search</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPrice32503OutOfStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.03</data> + <data key="quantity">25</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="weight">125.0000</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPrice245InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">245.00</data> + <data key="quantity">200</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">120.0000</data> + <data key="visibility">Catalog, Search</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPrice32501InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.01</data> + <data key="quantity">125</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">25.0000</data> + <data key="visibility">Catalog</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductTierPrice300InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">300.00</data> + <data key="quantity">34</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">1</data> + <data key="weightSelect">This item has weight</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductEnabledFlat" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">1.99</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="quantity">1000</data> + <data key="status">1</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">1</data> + <data key="weightSelect">This item has weight</data> + <data key="visibility">Catalog, Search</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPriceCustomOptions" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">245.00</data> + <data key="storefront_new_cartprice">343.00</data> + <data key="quantity">200</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">120.0000</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductDisabled" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">74.00</data> + <data key="quantity">87</data> + <data key="status">In Stock</data> + <data key="weight">333.0000</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductNotVisibleIndividually" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.00</data> + <data key="quantity">123</data> + <data key="status">In Stock</data> + <data key="weight">129.0000</data> + <data key="visibility">Not Visible Individually</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductDataOverriding" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">9.99</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="nameAndAttributeSkuMaskSimpleProduct" type="product"> + <data key="urlKey" unique="suffix">simple-product</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">10000.00</data> + <data key="quantity">657</data> + <data key="weight">50</data> + <data key="country_of_manufacture">UA</data> + <data key="country_of_manufacture_label">Ukraine</data> + <data key="type_id">simple</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="ProductShortDescription" type="ProductAttribute"> + <data key="attribute_code">short_description</data> + </entity> + <entity name="AddToDefaultSetTopOfContentSection" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">4</data> + <data key="attributeGroupId">13</data> + <data key="sortOrder">0</data> + </entity> + <entity name="productAlphabeticalA" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">AAA Product</data> + </entity> + <entity name="productAlphabeticalB" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">BBB Product</data> + </entity> + <entity name="productWithSpecialCharacters" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">Product "!@#$%^&*()+:;\|}{][?=~` </data> + <data key="nameWithSafeChars" unique="suffix">|}{][?=~` </data> + </entity> + <entity name="productWith130CharName" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">ProductWith128Chars 1234567891123456789112345678911234567891123456789112345678911234567891123456789112345678 endnums</data> + </entity> + <entity name="simpleProductDefault" type="product"> + <data key="sku" unique="suffix">sku_simple_product_</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Simple Product </data> + <data key="price">560</data> + <data key="urlKey" unique="suffix">simple-product-</data> + <data key="status">1</data> + <data key="quantity">25</data> + <data key="weight">1</data> + <data key="product_has_weight">1</data> + <data key="is_in_stock">1</data> + <data key="tax_class_id">2</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="simpleProductWithoutCategory" type="product"> + <data key="sku" unique="suffix">sku_simple_product_</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">560</data> + <data key="urlKey" unique="suffix">simple-product-</data> + <data key="status">1</data> + <data key="quantity">25</data> + <data key="weight">1</data> + <data key="product_has_weight">1</data> + <data key="is_in_stock">1</data> + <data key="tax_class_id">2</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml index ea0bcafe56c48..71c8af318e9b4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml @@ -12,4 +12,7 @@ <data key="from">10</data> <data key="to">100</data> </entity> + <entity name="ProductPerPage"> + <data key="productCount">1</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml new file mode 100644 index 0000000000000..000bb2095002c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml @@ -0,0 +1,19 @@ +<?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="RelatedProductLink" type="product_link"> + <var key="sku" entityKey="sku" entityType="product2"/> + <var key="linked_product_sku" entityKey="sku" entityType="product"/> + <data key="link_type">related</data> + <data key="linked_product_type">simple</data> + <data key="position">1</data> + <requiredEntity type="product_link_extension_attribute">Qty1000</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml new file mode 100644 index 0000000000000..bd4f807880ab8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml @@ -0,0 +1,14 @@ +<?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="OneRelatedProductLink" type="product_links"> + <requiredEntity type="product_link">RelatedProductLink</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/SimpleProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/SimpleProductOptionData.xml new file mode 100644 index 0000000000000..157a4d410263b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/SimpleProductOptionData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="simpleProductCustomizableOption"> + <data key="title" unique="suffix">Test3 option</data> + <data key="type">Drop-down</data> + <data key="is_required">1</data> + <data key="option_0_title" unique="suffix">40 Percent</data> + <data key="option_0_price">40.00</data> + <data key="option_0_price_type">Percent</data> + <data key="option_0_sku" unique="suffix">sku_drop_down_row_1</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml index 0aec1244d2650..e5070340421a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="testDataTierPrice" type="data"> <data key="goldenPrice1">$676.50</data> <data key="goldenPrice2">$615.00</data> @@ -20,4 +20,40 @@ <data key="name">secondStoreView</data> <data key="code">second_store_view</data> </entity> -</entities> + <entity name="tierPriceOnVirtualProduct" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">ALL GROUPS</data> + <data key="price">90.00</data> + <data key="qty">2</data> + </entity> + <entity name="tierPriceOnGeneralGroup" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">General</data> + <data key="price">80.00</data> + <data key="qty">2</data> + </entity> + <entity name="tierPriceOnDefault" type="data"> + <data key="website_0">All Websites [USD]</data> + <data key="customer_group_0">ALL GROUPS</data> + <data key="price_0">15.00</data> + <data key="qty_0">3</data> + <data key="website_1">All Websites [USD]</data> + <data key="customer_group_1">ALL GROUPS</data> + <data key="price_1">24.00</data> + <data key="qty_1">15</data> + </entity> + <entity name="tierPriceHighCostSimpleProduct" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">ALL GROUPS</data> + <data key="price">500,000.00</data> + <data key="qty">1</data> + </entity> + <entity name="tierProductPrice" type="catalogTierPrice"> + <data key="price">90.00</data> + <data key="price_type">fixed</data> + <data key="website_id">0</data> + <data key="customer_group">ALL GROUPS</data> + <data key="quantity">2</data> + <var key="sku" entityType="product2" entityKey="sku" /> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml new file mode 100644 index 0000000000000..fe1d49e4daadd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml @@ -0,0 +1,60 @@ +<?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="virtualProductCustomizableOption1"> + <data key="title" unique="suffix">Test1 option </data> + <data key="is_required">1</data> + <data key="type">Field</data> + <data key="option_0_price">120.03</data> + <data key="option_0_price_type">Fixed</data> + <data key="option_0_sku" unique="suffix">sku1_</data> + <data key="option_0_max_characters">45</data> + </entity> + <entity name="virtualProductCustomizableOption2"> + <data key="title" unique="suffix">Test2 option </data> + <data key="is_required">1</data> + <data key="type">Field</data> + <data key="option_0_price">120.03</data> + <data key="option_0_price_type">Fixed</data> + <data key="option_0_sku" unique="suffix">sku2_</data> + <data key="option_0_max_characters">45</data> + </entity> + <entity name="virtualProductCustomizableOption3"> + <data key="title" unique="suffix">Test3 option </data> + <data key="is_required">1</data> + <data key="type">Drop-down</data> + <data key="option_0_title" unique="suffix">Test3-1 </data> + <data key="option_0_price">110.01</data> + <data key="option_0_expected_price">9,900.90</data> + <data key="option_0_price_type">Percent</data> + <data key="option_0_sku" unique="suffix">sku3-1_</data> + <data key="option_0_sort_order">0</data> + <data key="option_1_title" unique="suffix">Test3-2 </data> + <data key="option_1_price">210.02</data> + <data key="option_1_price_type">Fixed</data> + <data key="option_1_sku" unique="suffix">sku3-2_</data> + <data key="option_1_sort_order">1</data> + </entity> + <entity name="virtualProductCustomizableOption4"> + <data key="title" unique="suffix">Test4 option </data> + <data key="is_required">1</data> + <data key="type">Drop-down</data> + <data key="option_0_title" unique="suffix">Test4-1 </data> + <data key="option_0_price">10.01</data> + <data key="option_0_price_type">Percent</data> + <data key="option_0_sku" unique="suffix">sku4-1_</data> + <data key="option_0_sort_order">0</data> + <data key="option_1_title" unique="suffix">Test4-2 </data> + <data key="option_1_price">20.02</data> + <data key="option_1_price_type">Fixed</data> + <data key="option_1_sku" unique="suffix">sku4-2_</data> + <data key="option_1_sort_order">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml new file mode 100644 index 0000000000000..18564ff101fd9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml @@ -0,0 +1,35 @@ +<?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="ProductLinkWidget" extends="ProductsListWidget"> + <data key="type">Catalog Product Link</data> + <data key="template">Product Link Block Template</data> + </entity> + <entity name="RecentlyComparedProductsWidget" type="widget"> + <data key="type">Recently Compared Products</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Recently Compared Products</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Sidebar Additional</data> + </entity> + <entity name="RecentlyViewedProductsWidget" type="widget"> + <data key="type">Recently Viewed Products</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Recently Viewed Products</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Sidebar Additional</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml index e16688ba0d37b..1ee57c89b2b31 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CatalogPriceConfigState" dataType="catalog_price_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> <object key="groups" dataType="catalog_price_config_state"> <object key="price" dataType="catalog_price_config_state"> @@ -21,4 +22,4 @@ </object> </object> </operation> -</operations> \ No newline at end of file +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml new file mode 100644 index 0000000000000..0fe4f154d5ef5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml @@ -0,0 +1,21 @@ +<?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="CatalogRecentlyProductsConfiguration" dataType="catalog_recently_products" type="create" + auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="catalog_recently_products"> + <object key="recently_products" dataType="catalog_recently_products"> + <object key="fields" dataType="catalog_recently_products"> + <object key="synchronize_with_backend" dataType="synchronize_with_backend"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml new file mode 100644 index 0000000000000..354277ad056f7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="catalogSpecialPrice" dataType="catalogSpecialPrice" type="create" auth="adminOauth" url="/V1/products/special-price" method="POST"> + <contentType>application/json</contentType> + <object key="prices" dataType="catalogSpecialPrice"> + <object dataType="catalogSpecialPrice" key="0"> + <field key="price">number</field> + <field key="store_id">integer</field> + <field key="sku">string</field> + <field key="price_from">string</field> + <field key="price_to">string</field> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_tier_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_tier_price-meta.xml new file mode 100644 index 0000000000000..7aa7530b0fda8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_tier_price-meta.xml @@ -0,0 +1,23 @@ +<?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="catalogTierPrice" dataType="catalogTierPrice" type="create" auth="adminOauth" url="/V1/products/tier-prices" method="POST"> + <contentType>application/json</contentType> + <object key="prices" dataType="catalogTierPrice"> + <object dataType="catalogTierPrice" key="0"> + <field key="price">number</field> + <field key="price_type">string</field> + <field key="website_id">integer</field> + <field key="sku">string</field> + <field key="customer_group">string</field> + <field key="quantity">integer</field> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml new file mode 100644 index 0000000000000..dd5d5aef08a7c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> + <section name="AdminNewWidgetSelectProductPopupSection"/> + <section name="AdminCatalogProductWidgetSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml index b3ed3f478f810..e4c4ece5ac6cf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml @@ -15,8 +15,10 @@ <section name="AdminProductImagesSection"/> <section name="AdminAddProductsToOptionPanel"/> <section name="AdminProductMessagesSection"/> + <section name="AdminProductAttributesSection"/> <section name="AdminProductFormRelatedUpSellCrossSellSection"/> <section name="AdminProductFormAdvancedPricingSection"/> <section name="AdminProductFormAdvancedInventorySection"/> + <section name="AdminAddAttributeModalSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductDeletePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductDeletePage.xml new file mode 100644 index 0000000000000..1ce53a0ebd54b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductDeletePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminProductDeletePage" url="catalog/product/delete/id/{{productId}}/" area="admin" module="Magento_Catalog" parameterized="true"> + <!-- This page object only exists for the url. Use the AdminProductCreatePage for selectors. --> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml new file mode 100644 index 0000000000000..3261db1f63f24 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.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="AdminCatalogProductWidgetSection"> + <element name="productAttributesToShow" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> + <element name="productButtonsToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml index 651ee54c08339..977e63b9ec927 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml @@ -22,7 +22,7 @@ <element name="ContentTab" type="input" selector="input[name='name']"/> <element name="FieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> <element name="panelFieldControl" type="input" selector='//aside//div[@data-index="{{arg1}}"]/descendant::*[@name="{{arg2}}"]' parameterized="true"/> - <element name="productsInCategory" type="input" selector="div[data-index='assign_products']"/> + <element name="productsInCategory" type="input" selector="div[data-index='assign_products']" timeout="30"/> </section> <section name="CategoryContentSection"> <element name="SelectFromGalleryBtn" type="button" selector="//label[text()='Select from Gallery']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml index 009110a729bde..e8adede5b2de6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml @@ -14,5 +14,7 @@ <element name="CategoryStoreViewDropdownToggle" type="button" selector="#store-change-button"/> <element name="CategoryStoreViewOption" type="button" selector="//div[contains(@class, 'store-switcher')]//a[normalize-space()='{{store}}']" parameterized="true"/> <element name="CategoryStoreViewModalAccept" type="button" selector=".modal-popup.confirm._show .action-accept"/> + <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> + <element name="storeSwitcher" type="text" selector=".store-switcher"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml index fee86ca1caa29..ea4f4bf53eb71 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCategoryMessagesSection"> <element name="SuccessMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector="//div[@class='message message-error error']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml index a449836fa08f4..df79ec61ef736 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml @@ -16,5 +16,6 @@ <element name="rowPosition" type="input" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-position .position input" timeout="30" parameterized="true"/> <element name="productGridNameProduct" type="text" selector="//table[@id='catalog_category_products_table']//td[contains(., '{{productName}}')]" parameterized="true"/> <element name="productVisibility" type="select" selector="//*[@name='product[visibility]']"/> + <element name="productSelectAll" type="checkbox" selector="input.admin__control-checkbox"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index ef6fb99e88eed..fba28b3feaff1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -13,8 +13,9 @@ <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> <element name="categoryTreeRoot" type="text" selector="div.x-tree-root-node>li.x-tree-node:first-of-type>div.x-tree-node-el:first-of-type" timeout="30"/> <element name="categoryInTree" type="text" selector="//a/span[contains(text(), '{{name}}')]" parameterized="true" timeout="30"/> - <element name="categoryInTreeUnderRoot" type="text" selector="//div[@class='x-tree-root-node']/li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> + <element name="categoryInTreeUnderRoot" type="text" selector="//li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> <element name="lastCreatedCategory" type="block" selector=".x-tree-root-ct li li:last-child" /> <element name="treeContainer" type="block" selector=".tree-holder" /> + <element name="expandRootCategory" type="text" selector="img.x-tree-elbow-end-plus"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml new file mode 100644 index 0000000000000..2de7bf19fd378 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml @@ -0,0 +1,33 @@ +<?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="AdminCreateNewProductAttributeSection"> + <element name="saveAttribute" type="button" selector="#save"/> + <element name="defaultLabel" type="input" selector="input[name='frontend_label[0]']"/> + <element name="inputType" type="select" selector="select[name='frontend_input']" timeout="30"/> + <element name="addValue" type="button" selector="//button[contains(@data-action,'add_new_row')]" timeout="30"/> + <element name="defaultStoreView" type="input" selector="//input[contains(@name,'option[value][option_{{row}}][1]')]" parameterized="true"/> + <element name="adminOption" type="input" selector="//input[contains(@name,'option[value][option_{{row}}][0]')]" parameterized="true"/> + <element name="defaultRadioButton" type="radio" selector="//tr[{{row}}]//input[contains(@name,'default[]')]" parameterized="true"/> + <element name="isRequired" type="checkbox" selector="//input[contains(@name,'is_required')]/..//label"/> + <element name="advancedAttributeProperties" type="text" selector="//div[contains(@data-index,'advanced_fieldset')]"/> + <element name="attributeCode" type="input" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//input[@name='attribute_code']"/> + <element name="scope" type="select" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//select[@name='is_global']" timeout="30"/> + <element name="defaultValue" type="input" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//input[@name='default_value_text']"/> + <element name="isUnique" type="checkbox" selector="//input[contains(@name, 'is_unique')]/..//label"/> + <element name="storefrontProperties" type="text" selector="//div[contains(@data-index,'front_fieldset')]"/> + <element name="inSearch" type="checkbox" selector="//input[contains(@name, 'is_searchable')]/..//label"/> + <element name="advancedSearch" type="checkbox" selector="//input[contains(@name, 'is_visible_in_advanced_search')]/..//label"/> + <element name="isComparable" type="checkbox" selector="//input[contains(@name, 'is_comparable')]/..//label"/> + <element name="allowHtmlTags" type="checkbox" selector="//input[contains(@name, 'is_html_allowed_on_front')]/..//label"/> + <element name="visibleOnStorefront" type="checkbox" selector="//input[contains(@name, 'is_visible_on_front')]/..//label"/> + <element name="sortProductListing" type="checkbox" selector="//input[contains(@name, 'is_visible_on_front')]/..//label"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml index 324f261f3a50a..d24c501152b78 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -22,6 +22,21 @@ <element name="TinyMCE4" type="button" selector="//span[text()='Default Value']/parent::label/following-sibling::div//div[@class='mce-branding-powered-by']"/> <element name="checkIfTabOpen" selector="//div[@id='advanced_fieldset-wrapper' and not(contains(@class,'opened'))]" type="button"/> <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> + <element name="dropdownAddOptions" type="button" selector="#add_new_option_button"/> + <!-- Manage Options nth child--> + <element name="dropdownNthOptionIsDefault" type="checkbox" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) .input-radio" parameterized="true"/> + <element name="dropdownNthOptionAdmin" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(3) input" parameterized="true"/> + <element name="dropdownNthOptionDefaultStoreView" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(4) input" parameterized="true"/> + <element name="dropdownNthOptionDelete" type="button" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) button[title='Delete']" parameterized="true"/> + </section> + <section name="AttributeDeleteModalSection"> + <element name="confirm" type="button" selector=".modal-popup.confirm .action-accept"/> + <element name="cancel" type="button" selector=".modal-popup.confirm .action-dismiss"/> + </section> + <section name="AttributeManageSwatchSection"> + <element name="swatchField" type="input" selector="//th[contains(@class, 'col-swatch')]/span[contains(text(), '{{arg}}')]/ancestor::thead/following-sibling::tbody//input[@placeholder='Swatch']" parameterized="true"/> + <element name="descriptionField" type="input" selector="//th[contains(@class, 'col-swatch')]/span[contains(text(), '{{arg}}')]/ancestor::thead/following-sibling::tbody//input[@placeholder='Description']" parameterized="true"/> </section> <section name="AttributeOptionsSection"> <element name="AddOption" type="button" selector="#add_new_option_button"/> @@ -31,6 +46,7 @@ <element name="StoreFrontPropertiesTab" selector="#product_attribute_tabs_front" type="button"/> <element name="EnableWYSIWYG" type="select" selector="#enabled"/> <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> + <element name="StorefrontPropertiesSectionToggle" type="button" selector="#front_fieldset-wrapper"/> </section> <section name="WYSIWYGProductAttributeSection"> <element name="ShowHideBtn" type="button" selector="#toggledefault_value_texteditor"/> @@ -72,9 +88,16 @@ <element name="AdvancedAttributePropertiesSectionToggle" type="button" selector="#advanced_fieldset-wrapper"/> <element name="AttributeCode" type="text" selector="#attribute_code"/> + <element name="DefaultValueText" type="textarea" selector="#default_value_text"/> + <element name="DefaultValueTextArea" type="textarea" selector="#default_value_textarea"/> + <element name="DefaultValueDate" type="textarea" selector="#default_value_date"/> + <element name="DefaultValueYesNo" type="textarea" selector="#default_value_yesno"/> <element name="Scope" type="select" selector="#is_global"/> + <element name="UniqueValue" type="select" selector="#is_unique"/> <element name="AddToColumnOptions" type="select" selector="#is_used_in_grid"/> <element name="UseInFilterOptions" type="select" selector="#is_filterable_in_grid"/> <element name="UseInProductListing" type="select" selector="#used_in_product_listing"/> + <element name="UseInSearch" type="select" selector="#is_searchable"/> + <element name="VisibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml index 5df17cb4f5a42..63bdcd52cdd20 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml @@ -18,6 +18,8 @@ <element name="AttributeDescription" type="text" selector="#description"/> <element name="ChangeAttributeDescriptionToggle" type="checkbox" selector="#toggle_description"/> <element name="Save" type="button" selector="button[title=Save]" timeout="30"/> + <element name="ProductDataMayBeLostModal" type="button" selector="//aside[contains(@class,'_show')]//header[contains(.,'Product data may be lost')]"/> + <element name="ProductDataMayBeLostConfirmButton" type="button" selector="//aside[contains(@class,'_show')]//button[.='Change Input Type']"/> <element name="defaultLabel" type="text" selector="//td[contains(text(), '{{attributeName}}')]/following-sibling::td[contains(@class, 'col-frontend_label')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml new file mode 100644 index 0000000000000..5329ad48c8f43 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -0,0 +1,14 @@ +<?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="AdminNewWidgetSection"> + <element name="selectProduct" type="button" selector=".btn-chooser" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml new file mode 100644 index 0000000000000..0da67849f85c6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml @@ -0,0 +1,15 @@ +<?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="AdminNewWidgetSelectProductPopupSection"> + <element name="filterBySku" type="input" selector=".data-grid-filters input[name='chooser_sku']"/> + <element name="firstRow" type="select" selector=".even>td" timeout="20"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAddAttributeModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAddAttributeModalSection.xml new file mode 100644 index 0000000000000..a3c98e43b4510 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAddAttributeModalSection.xml @@ -0,0 +1,19 @@ +<?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="AdminProductAddAttributeModalSection"> + <element name="addSelected" type="button" selector=".product_form_product_form_add_attribute_modal .page-main-actions .action-primary" timeout="30"/> + <element name="filters" type="button" selector=".product_form_product_form_add_attribute_modal button[data-action='grid-filter-expand']" timeout="30"/> + <element name="attributeCodeFilter" type="textarea" selector=".product_form_product_form_add_attribute_modal input[name='attribute_code']"/> + <element name="clearFilters" type="button" selector=".product_form_product_form_add_attribute_modal .action-clear" timeout="30"/> + <element name="firstRowCheckBox" type="input" selector=".product_form_product_form_add_attribute_modal .data-grid-checkbox-cell input"/> + <element name="applyFilters" type="button" selector=".product_form_product_form_add_attribute_modal .admin__data-grid-filters-footer .action-secondary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml index 160948f8f1f2c..5efd04eacb719 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -10,11 +10,18 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductAttributeGridSection"> <element name="AttributeCode" type="text" selector="//td[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> - <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> + <element name="createNewAttributeBtn" type="button" selector="#add"/> <element name="GridFilterFrontEndLabel" type="input" selector="#attributeGrid_filter_frontend_label"/> <element name="Search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> <element name="ResetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> <element name="FirstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]" timeout="30"/> <element name="FilterByAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> + <element name="attributeLabelFilter" type="input" selector="//input[@name='frontend_label']"/> + <element name="attributeCodeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-attr-code col-attribute_code')]"/> + <element name="defaultLabelColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-label col-frontend_label')]"/> + <element name="isVisibleColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_visible')]"/> + <element name="scopeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_global')]"/> + <element name="isSearchableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_searchable')]"/> + <element name="isComparableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_comparable')]"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml index 0f438540603d0..5f1112eef3625 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml @@ -10,5 +10,6 @@ <section name="DropdownAttributeOptionsSection"> <element name="nthOptionAdminLabel" type="input" selector="(//*[@id='manage-options-panel']//tr[{{var}}]//input[contains(@name, 'option[value]')])[1]" parameterized="true"/> + <element name="deleteButton" type="button" selector="(//td[@class='col-delete'])[1]" timeout="30"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml index b906e2fa9084b..3fad50adb771a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml @@ -14,5 +14,6 @@ <element name="nthRow" type="block" selector="#setGrid_table tbody tr:nth-of-type({{var1}})" parameterized="true"/> <element name="AttributeSetName" type="text" selector="//td[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="addAttributeSetBtn" type="button" selector="button.add-set" timeout="30"/> + <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml new file mode 100644 index 0000000000000..46a516b538f09 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributesSection"> + <element name="sectionHeader" type="button" selector="div[data-index='attributes']" timeout="30"/> + <element name="attributeLabelByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-label span" parameterized="true"/> + <element name="attributeTextInputByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-control input" parameterized="true"/> + <element name="attributeDropdownByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-control select" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml index 337cf0527dd4e..755add18ec1c0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml @@ -11,12 +11,10 @@ <section name="AdminProductCategoryCreationSection"> <element name="firstExampleProduct" type="button" selector=".data-row:nth-of-type(1)"/> <element name="newCategory" type="button" selector="//button/span[text()='New Category']"/> - <element name="nameInput" type="input" selector="input[name='name']"/> - <element name="parentCategory" type="block" selector=".product_form_product_form_create_category_modal div[data-role='selected-option']"/> <element name="parentSearch" type="input" selector="aside input[data-role='advanced-select-text']"/> <element name="parentSearchResult" type="block" selector="aside .admin__action-multiselect-menu-inner"/> - <element name="createCategory" type="button" selector="#save"/> + <element name="createCategory" type="button" selector="#save" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml index 784eff12a6c04..fafae5d535546 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductContentSection"> <element name="sectionHeader" type="button" selector="div[data-index='content']" timeout="30"/> + <element name="sectionHeaderShow" type="button" selector="div[data-index='content']._show" timeout="30"/> <element name="descriptionTextArea" type="textarea" selector="#product_form_description"/> <element name="shortDescriptionTextArea" type="textarea" selector="#product_form_short_description"/> <element name="sectionHeaderIfNotShowing" type="button" selector="//div[@data-index='content']//div[contains(@class, '_hide')]"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.xml new file mode 100644 index 0000000000000..803d72d7a7eca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.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="AdminProductCrossSellModalSection"> + <element name="addSelectedProducts" type="button" selector=".product_form_product_form_related_crosssell_modal .action-primary"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml index 052195ec1aaa7..352d219351fb8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml @@ -13,13 +13,15 @@ <element name="customizableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']"/> <element name="useDefaultOptionTitle" type="text" selector="[data-index='options'] tr.data-row [data-index='title'] [name^='options_use_default']"/> <element name="useDefaultOptionTitleByIndex" type="text" selector="[data-index='options'] [data-index='values'] tr[data-repeat-index='{{var1}}'] [name^='options_use_default']" parameterized="true"/> - <element name="addOptionBtn" type="button" selector="button[data-index='button_add']"/> + <element name="addOptionBtn" type="button" selector="button[data-index='button_add']" timeout="30"/> <element name="fillOptionTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Title']/parent::label/parent::div/parent::div//input[@class='admin__control-text']" parameterized="true"/> - <element name="optionTitleInput" type="input" selector="input[name='product[options][0][title]']"/> - <element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select"/> - <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li"/> + <element name="optionTitleInput" type="input" selector="input[name='product[options][{{index}}][title]']" parameterized="true"/> + <element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select" timeout="30"/> + <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li" timeout="30"/> <element name="maxCharactersInput" type="input" selector="input[name='product[options][0][max_characters]']"/> + <element name="optionTypeDropDown" type="select" selector="//table[@data-index='options']//tr[{{index}}]//div[@data-index='type']//div[contains(@class, 'action-select-wrap')]" parameterized="true" /> + <element name="optionTypeItem" type="select" selector="//table[@data-index='options']//tr[{{index}}]//div[@data-index='type']//*[contains(@class, 'action-menu-item')]//*[contains(., '{{optionValue}}')]" parameterized="true" /> <element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Type']/parent::label/parent::div/parent::div//div[@data-role='selected-option']" parameterized="true"/> <element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::div/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/> <element name="clickAddValue" type="button" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tfoot//button" parameterized="true"/> @@ -28,19 +30,30 @@ <element name="clickSelectPriceType" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody//tr[@data-repeat-index='{{var2}}']//span[text()='Price Type']/parent::label/parent::div/parent::div//select" parameterized="true"/> <element name="checkboxUseDefaultTitle" type="checkbox" selector="//span[text()='Option Title']/parent::label/parent::div/parent::div/div//input[@type='checkbox']"/> <element name="checkboxUseDefaultOption" type="checkbox" selector="//table[@data-index='values']//tbody//tr[@data-repeat-index='{{var1}}']//div[@class='admin__field-control']//input[@type='checkbox']" parameterized="true"/> + <element name="requiredCheckBox" type="checkbox" selector="input[name='product[options][{{index}}][is_require]']" parameterized="true" /> + <element name="fillOptionValueSku" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='SKU']/parent::label/parent::div/parent::div//div[@class='admin__field-control']/input" parameterized="true"/> <!-- Elements that make it easier to select the most recently added element --> <element name="lastOptionTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, '_required')]//input" /> <element name="lastOptionTypeParent" type="block" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__action-multiselect-text')]" /> <!-- var 1 represents the option type that you want to select, i.e "radio buttons" --> <element name="optionType" type="block" selector="//*[@data-index='custom_options']//label[text()='{{var1}}'][ancestor::*[contains(@class, '_active')]]" parameterized="true" /> - <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" /> + <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" timeout="30"/> <element name="valueTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='title']//input" /> <element name="valuePrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='price']//input" /> - - <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price]']"/> - <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price_type]']"/> - <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][sku]']"/> - <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][file_extension]']"/> + <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][price]']" parameterized="true"/> + <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{var}}][price_type]']" parameterized="true"/> + <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][sku]']" parameterized="true"/> + <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][file_extension]']" parameterized="true"/> + <element name="importOptions" type="button" selector="//button[@data-index='button_import']" timeout="30"/> + </section> + <section name="AdminProductImportOptionsSection"> + <element name="selectProductTitle" type="text" selector="//h1[contains(text(), 'Select Product')]" timeout="30"/> + <element name="filterButton" type="button" selector="//button[@data-action='grid-filter-expand']" timeout="30"/> + <element name="nameField" type="input" selector="//input[@name='name']" timeout="30"/> + <element name="applyFiltersButton" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> + <element name="resetFiltersButton" type="button" selector="//button[@data-action='grid-filter-reset']" timeout="30"/> + <element name="firstRowItemCheckbox" type="input" selector="//input[@data-action='select-row']" timeout="30"/> + <element name="importButton" type="button" selector="//button[contains(@class, 'action-primary')]/span[contains(text(), 'Import')]" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 0000000000000..b58fb2316f915 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.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="AdminProductDropdownOrderSection"> + <element name="simpleProduct" type="text" selector="//li[not(preceding-sibling::li)]//span[@title='Simple Product']"/> + <element name="virtualProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Bundle Product']]) and not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']]) and not(following-sibling::li[span[@title='Configurable Product']]) and not(following-sibling::li[span[@title='Grouped Product']])]/span[@title='Virtual Product']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml index aa752e0e2289c..1652546b0acb3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormActionSection"> <element name="backButton" type="button" selector="#back" timeout="30"/> + <element name="addAttributeButton" type="button" selector="#addAttribute" timeout="30"/> <element name="saveButton" type="button" selector="#save-button" timeout="30"/> <element name="saveArrow" type="button" selector="button[data-ui-id='save-button-dropdown']" timeout="30"/> <element name="saveAndClose" type="button" selector="span[title='Save & Close']" timeout="30"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml index 0314534dcddfb..bc7c472df6eac 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -17,9 +17,16 @@ <element name="qtyIncrements" type="input" selector="//input[@name='product[stock_data][qty_increments]']"/> <element name="qtyIncrementsUseConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_qty_increments]']"/> <element name="doneButton" type="button" selector="//aside[contains(@class,'product_form_product_form_advanced_inventory_modal')]//button[contains(@data-role,'action')]" timeout="5"/> + <element name="useConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_manage_stock]']"/> + <element name="manageStock" type="select" selector="//*[@name='product[stock_data][manage_stock]']"/> + <element name="advancedInventoryCloseButton" type="button" selector=".product_form_product_form_advanced_inventory_modal button.action-close" timeout="30"/> + <element name="miniQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_min_sale_qty]']"/> + <element name="miniQtyAllowedInCart" type="input" selector="//*[@name='product[stock_data][min_sale_qty]']"/> + <element name="maxiQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_max_sale_qty]']"/> + <element name="maxiQtyAllowedInCart" type="input" selector="//*[@name='product[stock_data][max_sale_qty]']"/> + <element name="notifyBelowQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_notify_stock_qty]']"/> + <element name="notifyBelowQty" type="input" selector="//*[@name='product[stock_data][notify_stock_qty]']"/> + <element name="advancedInventoryQty" type="input" selector="//div[@class='modal-inner-wrap']//input[@name='product[quantity_and_stock_status][qty]']"/> + <element name="advancedInventoryStockStatus" type="select" selector="//div[@class='modal-inner-wrap']//select[@name='product[quantity_and_stock_status][is_in_stock]']"/> </section> </sections> - - - - diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml index 0a1804aa284dc..3ef78a3fe8f41 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -14,12 +14,14 @@ <element name="advancedPricingCloseButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-close" timeout="30"/> <element name="productTierPriceWebsiteSelect" type="select" selector="[name='product[tier_price][{{var1}}][website_id]']" parameterized="true"/> <element name="productTierPriceCustGroupSelect" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]']" parameterized="true"/> + <element name="productTierPriceCustGroupSelectOptions" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]'] option" parameterized="true"/> <element name="productTierPriceQtyInput" type="input" selector="[name='product[tier_price][{{var1}}][price_qty]']" parameterized="true"/> <element name="productTierPriceValueTypeSelect" type="select" selector="[name='product[tier_price][{{var1}}][value_type]']" parameterized="true"/> <element name="productTierPriceFixedPriceInput" type="input" selector="[name='product[tier_price][{{var1}}][price]']" parameterized="true"/> <element name="productTierPricePercentageValuePriceInput" type="input" selector="[name='product[tier_price][{{var1}}][percentage_value]']" parameterized="true"/> <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="5"/> + <element name="msrp" type="input" selector="//input[@name='product[msrp]']" timeout="30"/> <element name="save" type="button" selector="#save-button"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml new file mode 100644 index 0000000000000..a2a349ed67611 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml @@ -0,0 +1,39 @@ +<?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="AdminProductFormAttributeSection"> + <element name="createNewAttribute" type="button" selector="//button[@data-index='add_new_attribute_button']" timeout="30"/> + </section> + <section name="AdminProductFormNewAttributeSection"> + <element name="attributeLabel" type="button" selector="//input[@name='frontend_label[0]']" timeout="30"/> + <element name="attributeType" type="select" selector="//select[@name='frontend_input']" timeout="30"/> + <element name="addValue" type="button" selector="//button[@data-action='add_new_row']" timeout="30"/> + <element name="optionViewName" type="text" selector="//table[@data-index='attribute_options_select']//span[contains(text(), '{{arg}}')]" parameterized="true" timeout="30"/> + <element name="optionValue" type="input" selector="(//input[contains(@name, 'option[value]')])[{{arg}}]" timeout="30" parameterized="true"/> + <element name="manageTitlesHeader" type="button" selector="//div[@class='fieldset-wrapper-title']//span[contains(text(), 'Manage Titles')]" timeout="30"/> + <element name="manageTitlesViewName" type="text" selector="//div[@data-index='manage-titles']//span[contains(text(), '{{arg}}')]" timeout="30" parameterized="true"/> + <element name="saveAttribute" type="button" selector="button#save" timeout="30"/> + <element name="saveInNewSet" type="button" selector="button#saveInNewSet" timeout="10"/> + </section> + <section name="AdminProductFormNewAttributeAdvancedSection"> + <element name="sectionHeader" type="button" selector="div[data-index='advanced_fieldset']"/> + <element name="defaultValue" type="textarea" selector="input[name='default_value_text']"/> + </section> + <section name="AdminProductFormNewAttributeStorefrontSection"> + <element name="sectionHeader" type="button" selector="div[data-index='front_fieldset']"/> + <element name="useInSearch" type="checkbox" selector="div[data-index='is_searchable'] .admin__field-control label"/> + <element name="searchWeight" type="select" selector="select[name='search_weight']"/> + </section> + <section name="AdminProductFormNewAttributeNewSetSection"> + <element name="setName" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//input[contains(@class, 'admin__control-text')]"/> + <element name="accept" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//button[contains(@class, 'action-accept')]"/> + <element name="cancel" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//button[contains(@class, 'action-dismiss')]"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 5791d0bfedab9..f515171e835db 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -13,23 +13,32 @@ <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> <element name="attributeSetFilterResultByName" type="text" selector="//label/span[text() = '{{var}}']" timeout="30" parameterized="true"/> <element name="productName" type="input" selector=".admin__field[data-index=name] input"/> + <element name="productNameDisabled" type="input" selector=".admin__field[data-index=name] input[disabled=true]"/> <element name="RequiredNameIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> <element name="RequiredSkuIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=sku]>.admin__field-label span'), ':after').getPropertyValue('content');"/> <element name="productSku" type="input" selector=".admin__field[data-index=sku] input"/> + <element name="productSkuDisabled" type="input" selector=".admin__field[data-index=sku] input[disabled=true]"/> <element name="enableProductAttributeLabel" type="text" selector="//span[text()='Enable Product']/parent::label"/> <element name="enableProductAttributeLabelWrapper" type="text" selector="//span[text()='Enable Product']/parent::label/parent::div"/> <element name="productStatus" type="checkbox" selector="input[name='product[status]']"/> + <element name="productStatusDisabled" type="checkbox" selector="input[name='product[status]'][disabled]"/> <element name="enableProductLabel" type="checkbox" selector="input[name='product[status]']+label"/> <element name="productStatusUseDefault" type="checkbox" selector="input[name='use_default[status]']"/> <element name="productNameUseDefault" type="checkbox" selector="input[name='use_default[name]']"/> <element name="productPrice" type="input" selector=".admin__field[data-index=price] input"/> + <element name="productPriceDisabled" type="input" selector=".admin__field[data-index=price] input[disabled=true]"/> + <element name="productPriceUseDefault" type="checkbox" selector=".admin__field[data-index=price] [name='use_default[price]']"/> <element name="productTaxClass" type="select" selector="//*[@name='product[tax_class_id]']"/> + <element name="productTaxClassDisabled" type="select" selector="select[name='product[tax_class_id]'][disabled=true]"/> <element name="productTaxClassUseDefault" type="checkbox" selector="input[name='use_default[tax_class_id]']"/> - <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']"/> + <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']" timeout="30"/> <element name="categoriesDropdown" type="multiselect" selector="div[data-index='category_ids']"/> + <element name="unselectCategories" type="button" selector="//span[@class='admin__action-multiselect-crumb']/span[contains(.,'{{category}}')]/../button[@data-action='remove-selected-item']" parameterized="true" timeout="30"/> <element name="productQuantity" type="input" selector=".admin__field[data-index=qty] input"/> - <element name="advancedInventoryLink" type="button" selector="//button[contains(@data-index, 'advanced_inventory_button')]"/> + <element name="advancedInventoryLink" type="button" selector="//button[contains(@data-index, 'advanced_inventory_button')]" timeout="30"/> <element name="productStockStatus" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]']"/> + <element name="productStockStatusDisabled" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]'][disabled=true]"/> + <element name="stockStatus" type="select" selector="[data-index='product-details'] select[name='product[quantity_and_stock_status][is_in_stock]']"/> <element name="productWeight" type="input" selector=".admin__field[data-index=weight] input"/> <element name="productWeightSelect" type="select" selector="select[name='product[product_has_weight]']"/> <element name="contentTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Content']"/> @@ -37,19 +46,30 @@ <element name="priceFieldError" type="text" selector="//input[@name='product[price]']/parent::div/parent::div/label[@class='admin__field-error']"/> <element name="addAttributeBtn" type="button" selector="#addAttribute"/> <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> - <element name="save" type="button" selector="#save"/> + <element name="save" type="button" selector="#save-button"/> + <element name="saveNewAttribute" type="button" selector="//aside[contains(@class, 'create_new_attribute_modal')]//button[@id='save']"/> + <element name="successMessage" type="text" selector="#messages"/> <element name="attributeTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Attributes']"/> <element name="attributeLabel" type="input" selector="//input[@name='frontend_label[0]']"/> <element name="frontendInput" type="select" selector="select[name = 'frontend_input']"/> <element name="productFormTab" type="button" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]" parameterized="true"/> <element name="productFormTabState" type="text" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]/parent::*/parent::*[@data-state-collapsible='{{state}}']" parameterized="true"/> <element name="visibility" type="select" selector="//select[@name='product[visibility]']"/> + <element name="visibilityDisabled" type="select" selector="select[name='product[visibility]'][disabled=true]"/> <element name="visibilityUseDefault" type="checkbox" selector="//input[@name='use_default[visibility]']"/> <element name="divByDataIndex" type="input" selector="div[data-index='{{var}}']" parameterized="true"/> <element name="setProductAsNewFrom" type="input" selector="input[name='product[news_from_date]']"/> <element name="setProductAsNewTo" type="input" selector="input[name='product[news_to_date]']"/> <element name="attributeLabelByText" type="text" selector="//*[@class='admin__field']//span[text()='{{attributeLabel}}']" parameterized="true"/> + <element name="attributeRequiredInput" type="input" selector="//input[contains(@name, 'product[{{attributeCode}}]')]" parameterized="true"/> + <element name="attributeFieldError" type="text" selector="//*[@class='admin__field _required _error']/..//label[contains(.,'This is a required field.')]"/> <element name="customSelectField" type="select" selector="//select[@name='product[{{var}}]']" parameterized="true"/> + <element name="searchCategory" type="input" selector="//*[@data-index='category_ids']//input[contains(@class, 'multiselect-search')]"/> + <element name="selectCategory" type="input" selector="//*[@data-index='category_ids']//label[contains(., '{{categoryName}}')]" parameterized="true"/> + <element name="done" type="button" selector="//*[@data-index='category_ids']//button[@data-action='close-advanced-select']" timeout="30"/> + <element name="selectMultipleCategories" type="input" selector="//*[@data-index='container_category_ids']//*[contains(@class, '_selected')]"/> + <element name="countryOfManufacture" type="select" selector="select[name='product[country_of_manufacture]']"/> + <element name="newAddedAttribute" type="text" selector="//fieldset[@class='admin__fieldset']//div[contains(@data-index,'{{attributeCode}}')]" parameterized="true"/> </section> <section name="ProductInWebsitesSection"> <element name="sectionHeader" type="button" selector="div[data-index='websites']" timeout="30"/> @@ -60,10 +80,13 @@ <element name="LayoutDropdown" type="select" selector="select[name='product[page_layout]']"/> </section> <section name="AdminProductFormRelatedUpSellCrossSellSection"> + <element name="relatedProductsHeader" type="button" selector=".admin__collapsible-block-wrapper[data-index='related']" timeout="30"/> <element name="AddRelatedProductsButton" type="button" selector="button[data-index='button_related']" timeout="30"/> + <element name="addUpSellProduct" type="button" selector="button[data-index='button_upsell']" timeout="30"/> </section> <section name="AdminAddRelatedProductsModalSection"> - <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_related_related_modal')]//button/span[contains(text(), 'Add Selected Products')]" timeout="30"/> + <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'related_modal')]//button[contains(@class, 'action-primary')]" timeout="30"/> + <element name="AddUpSellProductsButton" type="button" selector="//aside[contains(@class, 'upsell_modal')]//button[contains(@class, 'action-primary')]" timeout="30"/> </section> <section name="ProductWYSIWYGSection"> <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> @@ -176,5 +199,13 @@ <section name="AdminProductFormAdvancedPricingSection"> <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary"/> + <element name="useDefaultPrice" type="checkbox" selector="//input[@name='product[special_price]']/parent::div/following-sibling::div/input[@name='use_default[special_price]']"/> </section> -</sections> + <section name="AdminProductAttributeSection"> + <element name="attributeSectionHeader" type="button" selector="//div[@data-index='attributes']" timeout="30"/> + <element name="textAttributeByCode" type="text" selector="//input[@name='product[{{arg}}]']" parameterized="true"/> + <element name="textAttributeByName" type="text" selector="//div[@data-index='attributes']//fieldset[contains(@class, 'admin__field') and .//*[contains(.,'{{var}}')]]//input" parameterized="true"/> + <element name="dropDownAttribute" type="select" selector="//select[@name='product[{{arg}}]']" parameterized="true" timeout="30"/> + <element name="attributeSection" type="block" selector="//div[@data-index='attributes']/div[contains(@class, 'admin__collapsible-content _show')]" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml index 611f12a39b510..939974248aabf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml @@ -28,8 +28,12 @@ <element name="priceFilterTo" type="input" selector="input.admin__control-text[name='price[to]']"/> <element name="typeFilter" type="select" selector="select.admin__control-select[name='type_id']"/> <element name="statusFilter" type="select" selector="select.admin__control-select[name='status']"/> + <element name="firstRowBySku" type="button" selector="//div[text()='{{var}}']/ancestor::tr" parameterized="true" timeout="30"/> <element name="newFromDateFilter" type="input" selector="input.admin__control-text[name='news_from_date[from]']"/> <element name="keywordSearch" type="input" selector="input#fulltext"/> <element name="keywordSearchButton" type="button" selector=".data-grid-search-control-wrap button.action-submit" timeout="30"/> + <element name="nthRow" type="block" selector=".data-row:nth-of-type({{var}})" parameterized="true" timeout="30"/> + <element name="productCount" type="text" selector="#catalog_category_products-total-count"/> + <element name="productPerPage" type="select" selector="#catalog_category_products_page-limit"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index fe87c81ad6ac8..07dd26381fe08 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -8,7 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductGridSection"> - <element name="productRowBySku" type="block" selector="//div[@id='container']//tr//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> + <element name="productRowBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> + <element name="productRowCheckboxBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> <element name="loadingMask" type="text" selector=".admin__data-grid-loading-mask[data-component*='product_listing']"/> <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> @@ -16,6 +17,7 @@ <element name="productGridElement2" type="text" selector="#addselector" /> <element name="productGridRows" type="text" selector="table.data-grid tr.data-row"/> <element name="firstProductRow" type="text" selector="table.data-grid tr.data-row:first-of-type"/> + <element name="firstProductRowName" type="text" selector="table.data-grid tr.data-row:first-of-type > td:nth-of-type(4)"/> <element name="firstProductRowEditButton" type="button" selector="table.data-grid tr.data-row td .action-menu-item:first-of-type"/> <element name="productThumbnail" type="text" selector="table.data-grid tr:nth-child({{row}}) td.data-grid-thumbnail-cell > img" parameterized="true"/> <element name="productThumbnailBySrc" type="text" selector="img.admin__control-thumbnail[src*='{{pattern}}']" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml index adc3a753f06f5..dbdc82026947e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductModalSlideGridSection"> <element name="productGridXRowYColumnButton" type="input" selector=".modal-slide table.data-grid tr.data-row:nth-child({{row}}) td:nth-child({{column}})" parameterized="true" timeout="30"/> + <element name="productRowCheckboxBySku" type="input" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml index e90d806805f7c..ef596bed186e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml @@ -9,7 +9,10 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormRelatedUpSellCrossSellSection"> + <element name="sectionHeader" type="block" selector=".fieldset-wrapper.admin__collapsible-block-wrapper[data-index='related']"/> <element name="AddRelatedProductsButton" type="button" selector="button[data-index='button_related']" timeout="30"/> + <element name="AddUpSellProductsButton" type="button" selector="button[data-index='button_upsell']" timeout="30"/> + <element name="AddCrossSellProductsButton" type="button" selector="button[data-index='button_crosssell']" timeout="30"/> <element name="relatedProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='related']"/> <element name="upSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='upsell']"/> <element name="crossSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='crosssell']"/> @@ -18,4 +21,8 @@ <element name="selectedRelatedProduct" type="block" selector="//span[@data-index='name']"/> <element name="removeRelatedProduct" type="button" selector="//span[text()='Related Products']//..//..//..//span[text()='{{productName}}']//..//..//..//..//..//button[@class='action-delete']" parameterized="true"/> </section> + <section name="AdminAddUpSellProductsModalSection"> + <element name="Modal" type="button" selector=".product_form_product_form_related_upsell_modal"/> + <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_related_upsell_modal')]//button/span[contains(text(), 'Add Selected Products')]" timeout="30"/> + </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml index c545fcd408831..53231a2a68633 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml @@ -11,5 +11,6 @@ <section name="AdminProductSEOSection"> <element name="sectionHeader" type="button" selector="div[data-index='search-engine-optimization']" timeout="30"/> <element name="urlKeyInput" type="input" selector="input[name='product[url_key]']"/> + <element name="useDefaultUrl" type="checkbox" selector="input[name='use_default[url_key]']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml index bf8812b3acef5..53af1d5bd6eb1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml @@ -38,4 +38,8 @@ <element name="description" type="input" selector="#description"/> </section> + <section name="AdminUpdateAttributesWebsiteSection"> + <element name="website" type="button" selector="#attributes_update_tabs_websites"/> + <element name="addProductToWebsite" type="checkbox" selector="#add-products-to-website-content .website-checkbox"/> + </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml new file mode 100644 index 0000000000000..84a81c5204acc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.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="CatalogSubmenuSection"> + <element name="products" type="button" selector="//li[@id='menu-magento-catalog-catalog']//li[@data-ui-id='menu-magento-catalog-catalog-products']"/> + </section> +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml similarity index 81% rename from app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml rename to app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml index 42e451940c91b..b98bd47b54132 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewProductPageSection"> <element name="productName" type="input" selector="//input[@name='product[name]']"/> <element name="sku" type="input" selector="//input[@name='product[sku]']"/> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml similarity index 84% rename from app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml rename to app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml index 267efdf3d0e5e..ea37eb59b67f4 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductsPageSection"> <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> @@ -15,6 +15,5 @@ <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> - </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml new file mode 100644 index 0000000000000..7ce795c78f25b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryBottomToolbarSection"> + <element name="nextPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'next')]" timeout="30"/> + <element name="previousPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'previous')]" timeout="30"/> + <element name="pageNumber" type="text" selector="//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> + <element name="perPage" type="select" selector="//*[@class='toolbar toolbar-products'][2]//select[@id='limiter']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index 03566be55ad2f..1cd64544d9636 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -17,6 +17,7 @@ <element name="ProductItemInfo" type="button" selector=".product-item-info"/> <element name="specifiedProductItemInfo" type="button" selector="//a[@class='product-item-link'][contains(text(), '{{var1}}')]" parameterized="true"/> <element name="AddToCartBtn" type="button" selector="button.action.tocart.primary"/> + <element name="addToCartProductBySku" type="button" selector="//form[@data-product-sku='{{productSku}}']//button[contains(@class, 'tocart')]" parameterized="true" /> <element name="SuccessMsg" type="button" selector="div.message-success"/> <element name="productCount" type="text" selector="#toolbar-amount"/> <element name="CatalogDescription" type="text" selector="//div[@class='category-description']//p"/> @@ -25,9 +26,13 @@ <element name="productImage" type="text" selector="img.product-image-photo"/> <element name="productLink" type="text" selector="a.product-item-link"/> <element name="productLinkByHref" type="text" selector="a.product-item-link[href$='{{var1}}.html']" parameterized="true"/> - <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> + <element name="productPrice" type="text" selector=".price-final_price"/> <element name="categoryImage" type="text" selector=".category-image"/> <element name="emptyProductMessage" type="block" selector=".message.info.empty>div"/> <element name="lineProductName" type="text" selector=".products.list.items.product-items li:nth-of-type({{line}}) .product-item-link" timeout="30" parameterized="true"/> + <element name="asLowAs" type="input" selector="//*[@class='price-box price-final_price']/a/span[@class='price-container price-final_price tax weee']"/> + <element name="productsList" type="block" selector="#maincontent .column.main"/> + <element name="productName" type="text" selector=".product-item-name"/> + <element name="productOptionList" type="text" selector="#narrow-by-list"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml index 178e58ef2d649..51b5a0242d976 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -16,6 +16,7 @@ <element name="ProductInfoByNumber" type="text" selector="//main//li[{{var1}}]//div[@class='product-item-info']" parameterized="true"/> <element name="ProductAddToCompareByNumber" type="text" selector="//main//li[{{var1}}]//a[contains(@class, 'tocompare')]" parameterized="true"/> <element name="listedProduct" type="block" selector="ol li:nth-child({{productPositionInList}}) img" parameterized="true"/> + <element name="ProductImageByNumber" type="button" selector="//main//li[{{var1}}]//img" parameterized="true"/> <element name="categoryListView" type="button" selector="a[title='List']" timeout="30"/> <element name="ProductTitleByName" type="button" selector="//main//li//a[contains(text(), '{{var1}}')]" parameterized="true"/> @@ -31,5 +32,6 @@ <!--<element name="ProductAddToCompareByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/>--> <element name="ProductAddToCompareByName" type="text" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> <element name="ProductImageByNameAndSrc" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[contains(@src, '{{src}}')]" parameterized="true"/> + <element name="ProductStockUnavailable" type="text" selector="//*[text()='Out of stock']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml index 509ad2b8f849c..52a377ad264c0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,6 +9,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> - <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true"/> + <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml index 4dcda8dcd41ae..c58479a7b73e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -11,6 +11,6 @@ <section name="StorefrontMessagesSection"> <element name="success" type="text" selector="div.message-success.success.message"/> <element name="error" type="text" selector="div.message-error.error.message"/> - <element name="noticeMessage" type="text" selector="div.message-notice"/> + <element name="noticeMessage" type="text" selector="div.message.notice div"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml index ff2e5f2f36015..c6ea96715cf82 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontMiniCartSection"> <element name="quantity" type="button" selector="span.counter-number"/> <element name="show" type="button" selector="a.showcart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml index e8f35fc6787b7..292b2d7008bc1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml @@ -11,5 +11,7 @@ <element name="topCategory" type="button" selector="//a[contains(@class,'level-top')]/span[contains(text(),'{{var1}}')]" parameterized="true"/> <element name="subCategory" type="button" selector="//ul[contains(@class,'submenu')]//span[contains(text(),'{{var1}}')]" parameterized="true"/> <element name="breadcrumbs" type="textarea" selector=".items"/> + <element name="categoryBreadcrumbs" type="textarea" selector=".breadcrumbs li"/> + <element name="categoryBreadcrumbsByNumber" type="textarea" selector=".breadcrumbs li:nth-of-type({{number}})" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index b93a70559fc4a..8393cee57996f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,13 +13,15 @@ <element name="productName" type="text" selector=".base"/> <element name="productSku" type="text" selector=".product.attribute.sku>.value"/> <element name="productPriceLabel" type="text" selector=".price-label"/> - <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> + <element name="price" type="text" selector=".product-info-main [data-price-type='finalPrice']"/> + <element name="productPrice" type="text" selector=".price-final_price"/> <element name="qty" type="input" selector="#qty"/> <element name="specialPrice" type="text" selector=".special-price"/> + <element name="specialPriceAmount" type="text" selector=".special-price span.price"/> <element name="updatedPrice" type="text" selector="div.price-box.price-final_price [data-price-type='finalPrice'] .price"/> <element name="oldPrice" type="text" selector=".old-price"/> <element name="oldPriceTag" type="text" selector=".old-price .price-label"/> - <element name="oldPriceAmount" type="text" selector=".old-price .price"/> + <element name="oldPriceAmount" type="text" selector=".old-price span.price"/> <element name="productStockStatus" type="text" selector=".stock[title=Availability]>span"/> <element name="productImage" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[@class='fotorama__img']"/> <element name="productImageSrc" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[contains(@src, '{{src}}')]" parameterized="true"/> @@ -28,14 +30,17 @@ <element name="productOptionAreaInput" type="textarea" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//textarea" parameterized="true"/> <element name="productOptionFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'OptionFile')]/../div[@class='control']//input[@type='file']" parameterized="true"/> <element name="productOptionSelect" type="select" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//select" parameterized="true"/> - + <element name="asLowAs" type="input" selector="span[class='price-wrapper '] "/> + <element name="specialPriceValue" type="text" selector="//span[@class='special-price']//span[@class='price']"/> + <element name="mapPrice" type="text" selector="//div[@class='price-box price-final_price']//span[contains(@class, 'price-msrp_price')]"/> + <element name="clickForPriceLink" type="text" selector="//div[@class='price-box price-final_price']//a[contains(text(), 'Click for price')]"/> <!-- The parameter is the nth custom option that you want to get --> <element name="nthCustomOption" type="block" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]" parameterized="true" /> + <!-- The 1st parameter is the nth custom option, the 2nd parameter is the nth value in the option --> <element name="nthCustomOptionInput" type="radio" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]//*[contains(@class, 'admin__field-option')][{{customOptionValueNum}}]//input" parameterized="true" /> <element name="productOptionRadioButtonsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@price='{{var2}}']" parameterized="true"/> - <element name="productOptionDataMonth" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='month']" parameterized="true"/> <element name="productOptionDataDay" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='day']" parameterized="true"/> <element name="productOptionDataYear" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='year']" parameterized="true"/> @@ -49,7 +54,6 @@ <!-- Only one of Upload/Url Inputs are available for File and Sample depending on the value of the corresponding TypeSelector --> <element name="addLinkFileUploadFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@type='file']" parameterized="true" /> - <element name="productShortDescription" type="text" selector="//div[@class='product attribute overview']//div[@class='value']"/> <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> @@ -66,8 +70,21 @@ <element name="productOptionDropDownOptionTitle" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//select//option[contains(.,'{{var2}}')]" parameterized="true"/> <!-- Tier price selectors --> + <element name="tierPriceText" type="text" selector=".prices-tier li[class='item']" /> <element name="productTierPriceByForTextLabel" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}][contains(text(),'Buy {{var2}} for')]" parameterized="true"/> <element name="productTierPriceAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(text(), '{{var2}}')]" parameterized="true"/> <element name="productTierPriceSavePercentageAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(@class, 'percent')][contains(text(), '{{var2}}')]" parameterized="true"/> + + <!-- Special price selectors --> + <element name="productSpecialPrice" type="text" selector="//span[@data-price-type='finalPrice']/span"/> + <element name="specialProductText" type="text" selector="//span[text()='Regular Price']"/> + <element name="oldProductPrice" type="text" selector="//span[@data-price-type='oldPrice']/span"/> + + <!-- Customizable Option selectors --> + <element name="allCustomOptionLabels" type="text" selector="#product-options-wrapper label"/> + <element name="customOptionLabel" type="text" selector="//label[contains(., '{{customOptionTitle}}')]" parameterized="true"/> + <element name="customSelectOptions" type="select" selector="#{{selectId}} option" parameterized="true"/> + <element name="requiredCustomInput" type="text" selector="//div[contains(.,'{{customOptionTitle}}') and contains(@class, 'required') and .//input[@aria-required='true']]" parameterized="true"/> + <element name="requiredCustomSelect" type="select" selector="//div[contains(.,'{{customOptionTitle}}') and contains(@class, 'required') and .//select[@aria-required='true']]" parameterized="true"/> </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 45e0b03e8d995..ea10e12fb73f5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml @@ -9,6 +9,11 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductMediaSection"> + <element name="gallerySpinner" type="block" selector="#maincontent .fotorama__spinner--show" /> + <element name="gallery" type="block" selector="[data-gallery-role='gallery']" /> + <element name="productImage" type="text" selector="//*[@data-gallery-role='gallery' and not(contains(@class, 'fullscreen'))]//img[contains(@src, '{{filename}}') and not(contains(@class, 'full'))]" parameterized="true" /> + <element name="productImageFullscreen" type="text" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//img[contains(@src, '{{filename}}') and contains(@class, 'full')]" parameterized="true" /> + <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"/> </section> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index e9c8f53f97e5f..8055ecfe00cde 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -22,5 +22,6 @@ <element name="subTotal" type="input" selector="span[data-th='Subtotal']"/> <element name="shipping" type="input" selector="span[data-th='Shipping']"/> <element name="orderTotal" type="input" selector=".grand.totals .amount .price"/> + <element name="customOptionDropDown" type="select" selector="//*[@id='product-options-wrapper']//select[contains(@class, 'product-custom-option admin__control-select')]"/> </section> -</sections> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml new file mode 100644 index 0000000000000..f00abbe3c58c5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.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="StorefrontProductUpSellProductsSection"> + <element name="upSellHeading" type="text" selector="#block-upsell-heading"/> + <element name="upSellProducts" type="text" selector="div.upsell .product-item-name"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml new file mode 100644 index 0000000000000..87aab45bd8cb7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontWidgetsSection"> + <element name="widgetRecentlyViewedProductsGrid" type="block" selector=".block.widget.block-viewed-products-grid"/> + <element name="widgetRecentlyComparedProductsGrid" type="block" selector=".block.widget.block-compared-products-grid"/> + <element name="widgetRecentlyOrderedProductsGrid" type="block" selector=".block.block-reorder"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml new file mode 100644 index 0000000000000..4f66395bd0fbf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -0,0 +1,94 @@ +<?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="AddOutOfStockProductToCompareListTest"> + <annotations> + <features value="Catalog"/> + <title value="Add out of stock product to compare list"/> + <description value="Add out of stock product to compare list"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98644"/> + <useCaseId value="MAGETWO-98522"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct4" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo2"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open product page--> + <comment userInput="Open product page" stepKey="openProdPage"/> + <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPage"/> + <!--'Add to compare' link is not available--> + <comment userInput="'Add to compare' link is not available" stepKey="addToCompareLinkAvailability"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="dontSeeAddToCompareLink"/> + <!--Turn on 'out on stock' config--> + <comment userInput="Turn on 'out of stock' config" stepKey="onOutOfStockConfig"/> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="displayOutOfStockYes"/> + <!--Clear cache and reindex--> + <comment userInput="Clear cache and reindex" stepKey="cleanCache"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open product page--> + <comment userInput="Open product page" stepKey="openProductPage"/> + <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForSimpleProductPage2"/> + <!--Click on 'Add to Compare' link--> + <comment userInput="Click on 'Add to Compare' link" stepKey="clickOnAddToCompareLink"/> + <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickOnAddToCompare"/> + <waitForPageLoad stepKey="waitForProdAddToCmpList"/> + <!--Assert success message--> + <comment userInput="Assert success message" stepKey="assertSuccessMsg"/> + <grabTextFrom selector="{{AdminProductMessagesSection.successMessage}}" stepKey="grabTextFromSuccessMessage"/> + <assertEquals expected='You added product $$product.name$$ to the comparison list.' expectedType="string" actual="($grabTextFromSuccessMessage)" stepKey="assertSuccessMessage"/> + <!--See product in the comparison list--> + <comment userInput="See product in the comparison list" stepKey="seeProductInComparisonList"/> + <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage"/> + <waitForPageLoad stepKey="waitForStorefrontProductComparePageLoad"/> + <seeElement selector="{{StorefrontProductCompareMainSection.ProductLinkByName($product.name$)}}" stepKey="seeProductInCompareList"/> + <!--Go to Category page and delete product from comparison list--> + <comment userInput="Go to Category page and delete prduct from comparison list" stepKey="deletProdFromCmpList"/> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <click selector="{{StorefrontComparisonSidebarSection.ClearAll}}" stepKey="clickClearAll"/> + <waitForPageLoad time="30" stepKey="waitForConfirmPageLoad"/> + <click selector="{{AdminDeleteRoleSection.confirm}}" stepKey="confirmProdDelate"/> + <waitForPageLoad time="30" stepKey="waitForConfirmLoad"/> + <!--Add product to compare list from Category page--> + <comment userInput="Add product to compare list fom Category page" stepKey="addToCmpFromCategPage"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverOverProduct"/> + <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickAddToCompare"/> + <waitForPageLoad stepKey="waitProdAddingToCmpList"/> + <!--Assert success message--> + <comment userInput="Assert success message" stepKey="assertSuccessMsg2"/> + <grabTextFrom selector="{{AdminProductMessagesSection.successMessage}}" stepKey="grabTextFromSuccessMessage2"/> + <assertEquals expected='You added product $$product.name$$ to the comparison list.' expectedType="string" actual="($grabTextFromSuccessMessage)" stepKey="assertSuccessMessage2"/> + <!--Check that product displays on add to compare widget--> + <comment userInput="Check that product displays on add to compare widget" stepKey="checkProdNameOnWidget"/> + <seeElement selector="{{StorefrontComparisonSidebarSection.ProductTitleByName($$product.name$$)}}" stepKey="seeProdNameOnCmpWidget"/> + <!--See product in the compare page--> + <comment userInput="See product in the compare page" stepKey="seeProductInComparePage"/> + <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage2"/> + <waitForPageLoad stepKey="waitForStorefrontProductComparePageLoad2"/> + <seeElement selector="{{StorefrontProductCompareMainSection.ProductLinkByName($product.name$)}}" stepKey="seeProductInCompareList2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml new file mode 100644 index 0000000000000..53bb12fda4833 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml @@ -0,0 +1,93 @@ +<?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="AddToCartCrossSellTest"> + <annotations> + <features value="Catalog"/> + <stories value="Promote Products as Cross-Sells"/> + <title value="Admin should be able to add cross-sell to products."/> + <description value="Create products, add products to cross sells, and check that they appear in the Shopping Cart page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-9143"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="_defaultProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="_defaultProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="_defaultProduct" stepKey="simpleProduct3"> + <requiredEntity createDataKey="category1"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="logInAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimp1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimp2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimp3"/> + <deleteData createDataKey="category1" stepKey="deleteCategory"/> + </after> + + <!-- Go to simpleProduct1, add simpleProduct2 and simpleProduct3 as cross-sell--> + <amOnPage url="{{AdminProductEditPage.url($simpleProduct1.id$)}}" stepKey="goToProduct1"/> + <click stepKey="openHeader1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}"/> + + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct2ToSimp1"> + <argument name="sku" value="$simpleProduct2.sku$"/> + </actionGroup> + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct3ToSimp1"> + <argument name="sku" value="$simpleProduct3.sku$"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + + <!-- Go to simpleProduct3, add simpleProduct1 and simpleProduct2 as cross-sell--> + <amOnPage url="{{AdminProductEditPage.url($simpleProduct3.id$)}}" stepKey="goToProduct3"/> + <click stepKey="openHeader2" selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}"/> + + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct1ToSimp3"> + <argument name="sku" value="$simpleProduct1.sku$"/> + </actionGroup> + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct2ToSimp3"> + <argument name="sku" value="$simpleProduct2.sku$"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave2"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <!-- Go to frontend, add simpleProduct1 to cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimp1ToCart"> + <argument name="product" value="$simpleProduct1$"/> + </actionGroup> + + <!-- Check that cart page contains cross-sell to simpleProduct2 and simpleProduct3--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart1"/> + <waitForPageLoad stepKey="waitForCartToLoad"/> + <waitForElementVisible selector="{{CheckoutCartCrossSellSection.products}}" stepKey="waitForCrossSellLoading"/> + <see stepKey="seeProduct2InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> + <see stepKey="seeProduct3InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct3.name$"/> + + <!-- Add simpleProduct3 to cart, check cross-sell contains product2 but not product3--> + <click stepKey="addSimp3ToCart" selector="{{CheckoutCartCrossSellSection.productRowByName($simpleProduct3.name$)}}{{CheckoutCartCrossSellSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForCartToLoad2"/> + <see stepKey="seeProduct2StillInCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> + <dontSee stepKey="dontSeeProduct3InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct3.name$"/> + + <!-- Add simpleProduct2 to cart, check cross-sell doesn't contain product 2 anymore.--> + <click stepKey="addSimp2ToCart" selector="{{CheckoutCartCrossSellSection.productRowByName($simpleProduct2.name$)}}{{CheckoutCartCrossSellSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForCartToLoad3"/> + <dontSee stepKey="dontSeeProduct2InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml index 88a39a9087bb3..117f094ee0607 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml @@ -43,7 +43,9 @@ <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> <!-- Assert product image in admin product form --> - <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"/> + <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> <!-- Assert product in storefront product page --> <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml index 5456cb02e74ca..f657fbbdae607 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml index f48c352c5290a..eab36bc90dc18 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoVirtualProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml new file mode 100644 index 0000000000000..e3f4d6cbdde0d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -0,0 +1,87 @@ +<?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="AdminAddInStockProductToTheCartTest"> + <annotations> + <stories value="Manage products"/> + <title value="Add In Stock Product to Cart"/> + <description value="Login as admin and add In Stock product to the cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11065"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create Simple Product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Product Index Page and filter the product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Update product Advanced Inventory setting --> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="In Stock" stepKey="selectOutOfStock"/> + <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Verify product is visible in category front page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + <!--Verify Product In Store Front--> + <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> + <!--Add Product to the cart--> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> + <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInMiniCart"/> + <seeElement selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="seeCheckOutButtonInMiniCart"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml new file mode 100644 index 0000000000000..a51df86d0327a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCatalogCategoriesNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin catalog categories navigate menu test"/> + <description value="Admin should be able to navigate to Catalog > Categories"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14131"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCategoriesPage"> + <argument name="menuUiId" value="{{AdminMenuCatalog.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCatalogCategories.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCatalogCategories.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml new file mode 100644 index 0000000000000..1d9400bf81e4d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCatalogProductsNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin catalog products navigate menu test"/> + <description value="Admin should be able to navigate to Catalog > Products"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14130"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogProductsPage"> + <argument name="menuUiId" value="{{AdminMenuCatalog.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCatalogProducts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCatalogProducts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml new file mode 100644 index 0000000000000..bcfab6ccfdf1f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml @@ -0,0 +1,66 @@ +<?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="AdminChangeProductAttributeSet"> + <annotations> + <features value="Checkout"/> + <stories value="The required product attribute is not displayed when change attribute set"/> + <title value="Attributes from the selected attribute set should be shown"/> + <description value="Attributes from the selected attribute set should be shown"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-98452"/> + <useCaseId value="MAGETWO-98357"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="productAttributeWithTwoOptions" stepKey="createProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createProductAttributeOption1"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createProductAttributeOption2"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$$createAttributeSet.attribute_set_id$$/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + </after> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="searchForAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + + <waitForText userInput="$$createProductAttribute.default_frontend_label$$" stepKey="seeAttributeInForm"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml new file mode 100644 index 0000000000000..86978a4121a43 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml @@ -0,0 +1,180 @@ +<?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="AdminCheckConfigurableProductPriceWithDisabledChildProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check Price for Configurable Product when One Child is Disabled, Others are Enabled"/> + <description value="Login as admin and check the configurable product price when one child product is disabled "/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13749"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + + <!-- Create Default Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create an attribute with three options to be used in the first child product --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the third option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">10.00</field> + </createData> + + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <field key="price">20.00</field> + </createData> + + <!--Create a simple product and give it the attribute with the Third option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <field key="price">30.00</field> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Add the third simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + </before> + <after> + <!-- Delete Created Data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product in Store Front Page --> + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!-- Verify category,Configurable product and initial price --> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeInitialPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> + + <!-- Verify First Child Product attribute option is displayed --> + <see selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="seeOption1"/> + + <!-- Select product Attribute option1, option2 and option3 and verify changes in the price --> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="selectOption1"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeChildProduct1PriceInStoreFront"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="selectOption2"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeChildProduct2PriceInStoreFront"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="selectOption3"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct3.price$$" stepKey="seeChildProduct3PriceInStoreFront"/> + + <!-- Open Product Index Page and Filter First Child product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="ApiSimpleOne"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="selectFirstRow"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + + <!-- Disable the product --> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!-- Open Product Store Front Page --> + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront1"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + + <!-- Verify category,configurable product and updated price --> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeUpdatedProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront1"/> + + <!-- Verify product Attribute Option1 is not displayed --> + <dontSee selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="dontSeeOption1"/> + + <!--Select product Attribute option2 and option3 and verify changes in the price --> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="selectTheOption2"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeSecondChildProductPriceInStoreFront"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect($$createConfigProductAttribute.default_value$$)}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="selectTheOption3"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct3.price$$" stepKey="seeThirdProductPriceInStoreFront"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml new file mode 100644 index 0000000000000..8d41b276334a6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml @@ -0,0 +1,25 @@ +<?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="AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest" extends="AdminCheckConfigurableProductPriceWithDisabledChildProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check Price for Configurable Product when Child is Out of Stock"/> + <description value="Login as admin and check the configurable product price when one child product is out of stock "/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13750"/> + <group value="mtf_migrated"/> + </annotations> + + <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity" after="waitForProductPageToLoad"/> + <remove keyForRemoval="disableProduct"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock" after="scrollToProductQuantity"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml new file mode 100644 index 0000000000000..fd22142fcb097 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -0,0 +1,53 @@ +<?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="AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest"> + <annotations> + <stories value="Create category"/> + <title value="Inactive Category and subcategory are not visible on navigation menu, Include in Menu = No"/> + <description value="Login as admin and verify inactive and inactive include in menu category and subcategory is not visible in navigation menu"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13638"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create Parent Inactive and Not Include In Menu Category --> + <createData entity="CatInactiveNotInMenu" stepKey="createCategory"/> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Create subcategory under parent category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!-- Verify Parent Category and Sub category is not visible in navigation menu --> + <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml new file mode 100644 index 0000000000000..b6c76d6577210 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest"> + <annotations> + <stories value="Create category"/> + <title value="Inactive Category and subcategory are not visible on navigation menu, Include in Menu = Yes"/> + <description value="Login as admin and verify inactive category and subcategory is not visible in navigation menu"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13637"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create Parent Inactive Category --> + <createData entity="CatNotActive" stepKey="createCategory"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Create subcategory under parent category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!-- Verify Parent Category and Sub category is not visible in navigation menu --> + <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml new file mode 100644 index 0000000000000..c9cd9acd9708c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -0,0 +1,53 @@ +<?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="AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest"> + <annotations> + <stories value="Create category"/> + <title value="Active Category and subcategory are not visible on navigation menu, Include in Menu = No"/> + <description value="Login as admin and verify inactive include in menu category and subcategory is not visible in navigation menu"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13636"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create inactive Include In Menu Parent Category --> + <createData entity="CatNotIncludeInMenu" stepKey="createCategory"/> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Create subcategory under parent category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!-- Verify Parent Category and Sub category is not visible in navigation menu --> + <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml new file mode 100644 index 0000000000000..ee8b48a94b20d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckOutOfStockProductIsNotVisibleInCategoryTest"> + <annotations> + <stories value="Manage products"/> + <title value="Out of Stock Product is Not Visible in Category"/> + <description value="Login as admin and check out of stock product is not visible in category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11064"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create Simple Product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Product Index Page and filter the product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Update product Advanced Inventory Setting --> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> + <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> + <waitForPageLoad stepKey="waitForProductPageToSave"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Verify product is not visible in category store front page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="dontSeeProductInCategoryPage"/> + <!--Verify Product In Store Front--> + <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToProductStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductPageTobeLoaded"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="Out of stock" stepKey="seeProductStatusIsOutOfStock"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml new file mode 100644 index 0000000000000..e1cb45be22b4e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -0,0 +1,73 @@ +<?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="AdminCheckOutOfStockProductIsVisibleInCategoryTest"> + <annotations> + <stories value="Manage products"/> + <title value="Out of Stock Product is Visible in Category"/> + <description value="Login as admin and check out of stock product is visible in category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11067"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Set Display out of stock product--> + <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 1" /> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create Simple Product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0" /> + </after> + <!--Open Product Index Page and filter the product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Update product Advanced Inventory Setting --> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuantityUsesDecimal"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> + <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Verify product is visible in category front page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml new file mode 100644 index 0000000000000..f40a62c164ecc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -0,0 +1,176 @@ +<?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="AdminCheckPaginationInStorefrontTest"> + <annotations> + <stories value="Create flat catalog product"/> + <title value="Verify that pagination works when Flat Category is enabled"/> + <description value="Login as admin, create flat catalog product and check pagination"/> + <testCaseId value="MC-6051"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <group value="Catalog"/> + </annotations> + <before> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1 "/> + <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 1 "/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="PaginationProduct" stepKey="simpleProduct1"/> + <createData entity="PaginationProduct" stepKey="simpleProduct2"/> + <createData entity="PaginationProduct" stepKey="simpleProduct3"/> + <createData entity="PaginationProduct" stepKey="simpleProduct4"/> + <createData entity="PaginationProduct" stepKey="simpleProduct5"/> + <createData entity="PaginationProduct" stepKey="simpleProduct6"/> + <createData entity="PaginationProduct" stepKey="simpleProduct7"/> + <createData entity="PaginationProduct" stepKey="simpleProduct8"/> + <createData entity="PaginationProduct" stepKey="simpleProduct9"/> + <createData entity="PaginationProduct" stepKey="simpleProduct10"/> + <createData entity="PaginationProduct" stepKey="simpleProduct11"/> + <createData entity="PaginationProduct" stepKey="simpleProduct12"/> + <createData entity="PaginationProduct" stepKey="simpleProduct13"/> + <createData entity="PaginationProduct" stepKey="simpleProduct14"/> + <createData entity="PaginationProduct" stepKey="simpleProduct15"/> + <createData entity="PaginationProduct" stepKey="simpleProduct16"/> + <createData entity="PaginationProduct" stepKey="simpleProduct17"/> + <createData entity="PaginationProduct" stepKey="simpleProduct18"/> + <createData entity="PaginationProduct" stepKey="simpleProduct19"/> + <createData entity="PaginationProduct" stepKey="simpleProduct20"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0" /> + <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0" /> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> + <deleteData createDataKey="simpleProduct6" stepKey="deleteSimpleProduct6"/> + <deleteData createDataKey="simpleProduct7" stepKey="deleteSimpleProduct7"/> + <deleteData createDataKey="simpleProduct8" stepKey="deleteSimpleProduct8"/> + <deleteData createDataKey="simpleProduct9" stepKey="deleteSimpleProduct9"/> + <deleteData createDataKey="simpleProduct10" stepKey="deleteSimpleProduct10"/> + <deleteData createDataKey="simpleProduct11" stepKey="deleteSimpleProduct11"/> + <deleteData createDataKey="simpleProduct12" stepKey="deleteSimpleProduct12"/> + <deleteData createDataKey="simpleProduct13" stepKey="deleteSimpleProduct13"/> + <deleteData createDataKey="simpleProduct14" stepKey="deleteSimpleProduct14"/> + <deleteData createDataKey="simpleProduct15" stepKey="deleteSimpleProduct15"/> + <deleteData createDataKey="simpleProduct16" stepKey="deleteSimpleProduct16"/> + <deleteData createDataKey="simpleProduct17" stepKey="deleteSimpleProduct17"/> + <deleteData createDataKey="simpleProduct18" stepKey="deleteSimpleProduct18"/> + <deleteData createDataKey="simpleProduct19" stepKey="deleteSimpleProduct19"/> + <deleteData createDataKey="simpleProduct20" stepKey="deleteSimpleProduct20"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page and select created category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForPageToLoad0"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForPageToLoaded2"/> + + <!--Select Products--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <waitForPageLoad stepKey="waitForProductsToLoad"/> + <scrollTo selector="{{CatalogProductsSection.resetFilter}}" stepKey="scrollToResetFilter"/> + <waitForElementVisible selector="{{CatalogProductsSection.resetFilter}}" time="30" stepKey="waitForResetButtonToVisible"/> + <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <selectOption selector="{{AdminProductGridFilterSection.productPerPage}}" userInput="20" stepKey="selectPagePerView"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="pagi" stepKey="selectProduct1"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitFroPageToLoad1"/> + <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="20" stepKey="seeNumberOfProductsFound"/> + <click selector="{{AdminCategoryProductsGridSection.productSelectAll}}" stepKey="selectSelectAll"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> + + <!--Open Category Store Front Page--> + <amOnPage url="{{_defaultCategory.name}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Select 9 items per page and verify number of products displayed in each page --> + <conditionalClick selector="{{StorefrontCategoryTopToolbarSection.gridMode}}" visible="true" dependentSelector="{{StorefrontCategoryTopToolbarSection.gridMode}}" stepKey="seeProductGridIsActive"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToBottomToolbarSection"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="9" stepKey="selectPerPageOption"/> + + <!--Verify number of products displayed in First Page --> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage"/> + + <!--Verify number of products displayed in Second Page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage"/> + + <!--Verify number of products displayed in third Page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton1"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage"/> + + <!--Change Pages using Previous Page selector and verify number of products displayed in each page--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad5"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage1"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage1"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad6"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage1"/> + + <!--Select Pages by using page Number and verify number of products displayed--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage2"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('2')}}" stepKey="clickOnPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad7"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage2"/> + + <!--Select Third Page using page number--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage3"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('3')}}" stepKey="clickOnThirdPage"/> + <waitForPageLoad stepKey="waitForPageToLoad8"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage2"/> + + <!--Select First Page using page number--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage4"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage"/> + <waitForPageLoad stepKey="waitForPageToLoad9"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsFirstPage2"/> + + <!--Select 15 items per page and verify number of products displayed in each page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="15" stepKey="selectPerPageOption1"/> + <waitForPageLoad stepKey="waitForPageToLoad10"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="15" stepKey="seeNumberOfProductsInFirstPage3"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton2"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad11"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="5" stepKey="seeNumberOfProductsInSecondPage3"/> + + <!--Select First Page using page number--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="scrollToPreviousPage5"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad13"/> + + <!--Select 30 items per page and verify number of products displayed in each page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage4"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="30" stepKey="selectPerPageOption2"/> + <waitForPageLoad stepKey="waitForPageToLoad12"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="20" stepKey="seeNumberOfProductsInFirstPage4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml new file mode 100644 index 0000000000000..f5872ac3efca0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest"> + <annotations> + <stories value="Create category"/> + <title value="Active category is visible on navigation menu while subcategory is not visible on navigation menu, Include in Menu = Yes"/> + <description value="Login as admin and verify subcategory is not visible in navigation menu"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13635"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create Parent Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Create subcategory under parent category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!-- Verify Parent Category is visible in navigation menu and Sub category is not visible in navigation menu --> + <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml new file mode 100755 index 0000000000000..4deca73504677 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -0,0 +1,83 @@ +<?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="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from simple to virtual"/> + <description value="After selecting a simple product when adding Admin should be switch to virtual implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10925"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <!-- Open Dropdown and select simple product option --> + <comment stepKey="beforeOpenProductFillForm" userInput="Selecting Product from the Add Product Dropdown"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="simple"/> + </actionGroup> + + <!-- Fill form for Virtual Product Type --> + <comment stepKey="beforeFillProductForm" userInput="Filling Product Form"/> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="SetProductUrlKey" stepKey="setProductUrl"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + <!-- Check that product was added with implicit type change --> + <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <actionGroup ref="filterProductGridByName" stepKey="searchForProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + </test> + <test name="AdminCreateVirtualProductSwitchToSimpleTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from virtual to simple"/> + <description value="After selecting a virtual product when adding Admin should be switch to simple implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10928"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="virtual"/> + </actionGroup> + <!-- Fill form for Virtual Product Type --> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml new file mode 100644 index 0000000000000..d9e410a9a3009 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml @@ -0,0 +1,70 @@ +<?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="AdminCreateAttributeSetEntityTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Attribute Set"/> + <title value="Create attribute set with new product attribute"/> + <description value="Admin should be able to create attribute set with new product attribute"/> + <testCaseId value="MC-10884"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + </after> + + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="goToAttributeSetByName" stepKey="filterProductAttributeSetGridByLabel"> + <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> + </actionGroup> + + <!-- Assert created attribute in an unassigned attributes --> + <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> + + <!-- Assign attribute in the group --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets2"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <!-- Assert an attribute in the group--> + <actionGroup ref="goToAttributeSetByName" stepKey="filterProductAttributeSetGridByLabel2"> + <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> + </actionGroup> + <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup2"/> + + <!-- Assert attribute can be used in product creation --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + + <!-- Switch from default attribute set to new attribute set --> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="searchForAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + + <!-- See new attribute set --> + <see selector="{{AdminProductFormSection.attributeSet}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="seeAttributeSetName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml index 7c24a8aba27bd..79eec02a828f6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml @@ -28,6 +28,7 @@ <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct2"> <argument name="product" value="SimpleProduct"/> </actionGroup> + <actionGroup ref="NavigateToAndResetProductGridToDefaultView" stepKey="NavigateToAndResetProductGridToDefaultView"/> <actionGroup ref="logout" stepKey="logout"/> </after> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml new file mode 100644 index 0000000000000..a3f543e9cf32a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -0,0 +1,142 @@ +<?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="AdminCreateCustomProductAttributeWithDropdownFieldTest"> + <annotations> + <stories value="Create product Attribute"/> + <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> + <description value="login as admin and create configurable product attribute with Dropdown field"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10905"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MC-15474"/> + </skip> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!--Create Configurable Product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + + <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> + <argument name="ProductAttribute" value="newProductAttribute"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!-- Select Created Product--> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createConfigProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> + + <!-- Create New Product Attribute --> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> + <waitForPageLoad stepKey="waitForAttributePageToLoad"/> + <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> + <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Dropdown" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="waitForAddValueButtonToVisible"/> + <click selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="clickOnAddValueButton"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultStoreView('0')}}" stepKey="waitForDefaultStoreViewToVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultStoreView('0')}}" userInput="{{ProductAttributeOption8.label}}" stepKey="fillDefaultStoreView"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.adminOption('0')}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAdminField"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.defaultRadioButton('1')}}" stepKey="selectRadioButton"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="scrollToAdvancedAttributeProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties1"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> + <waitForPageLoad stepKey="waitForStoreFrontPropertiesTodiaplay"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" x="0" y="-80" stepKey="scroll1ToSortProductListing"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isComparable}}" stepKey="enableComparableOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.allowHtmlTags}}" stepKey="enableAllowHtmlTags"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> + <waitForPageLoad stepKey="waitForProductToSave"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Verify product attribute added in product form --> + <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> + <click selector="{{AdminProductFormSection.attributeTab}}" stepKey="clickOnAttribute"/> + <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> + + <!--Verify Product Attribute in Attribute Form --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="seeAttributeCode"/> + <see selector="{{AdminProductAttributeGridSection.defaultLabelColumn}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeDefaultLabel"/> + <see selector="{{AdminProductAttributeGridSection.isVisibleColumn}}" userInput="Yes" stepKey="seeIsVisibleColumn"/> + <see selector="{{AdminProductAttributeGridSection.isSearchableColumn}}" userInput="Yes" stepKey="seeSearchableColumn"/> + <see selector="{{AdminProductAttributeGridSection.isComparableColumn}}" userInput="Yes" stepKey="seeComparableColumn"/> + + <!--Verify Product Attribute is present in Category Store Front Page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> + <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> + <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> + + <!--Verify Product Attribute present in search page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage1"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad1"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAttribute"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearchResultToLoad"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInCategoryPage"/> + <see selector="{{StorefrontCategoryMainSection.productOptionList}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeProductAttributeOptionInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml new file mode 100644 index 0000000000000..1bc69be642a37 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -0,0 +1,87 @@ +<?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="AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create product Dropdown attribute and check its visibility on frontend in Advanced Search form"/> + <title value="AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest"/> + <description value="Admin should able to create product Dropdown attribute and check its visibility on frontend in Advanced Search form"/> + <testCaseId value="MC-10827"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create product attribute with 2 options --> + <createData entity="productDropDownAttributeNotSearchable" stepKey="attribute"/> + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="option2"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <!-- Create product attribute set --> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Filter product attribute set by attribute set name --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> + <actionGroup ref="FilterProductAttributeSetGridByAttributeSetName" stepKey="filterProductAttrSetGridByAttrSetName"> + <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> + </actionGroup> + + <!-- Assert created attribute in an unassigned attributes --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> + + <!-- Assign attribute in the group --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + + <!-- Go to Product Attribute Grid page --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> + + <!-- Change attribute property: Frontend Label --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel"/> + + <!-- Change attribute property: Use in Search >Yes --> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="seeInSearch"/> + + <!-- Change attribute property: Visible In Advanced Search >No --> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.VisibleInAdvancedSearch}}" userInput="No" stepKey="dontSeeInAdvancedSearch"/> + + <!-- Save the new product attributes --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Go to store's advanced catalog search page --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <dontSeeElement selector="{{StorefrontCatalogSearchAdvancedFormSection.AttributeByCode('$$attribute.attribute_code$$')}}" stepKey="dontSeeAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml new file mode 100644 index 0000000000000..37ec4e0d32528 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml @@ -0,0 +1,41 @@ +<?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="AdminCreateDuplicateCategoryTest"> + <annotations> + <stories value="Create category"/> + <title value="Create Duplicate Category With Already Existed UrlKey"/> + <description value="Login as admin and create duplicate category"/> + <testCaseId value="MC-14702"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category Page and select Add category --> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCategoryPage"/> + + <!-- Fill the Category form with same name and urlKey as initially created category(SimpleSubCategory) --> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillCategoryForm"> + <argument name="categoryName" value="$$category.name$$"/> + <argument name="categoryUrlKey" value="$$category.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Assert error message --> + <see selector="{{AdminCategoryMessagesSection.errorMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml new file mode 100644 index 0000000000000..575bb56912b25 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml @@ -0,0 +1,61 @@ +<?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="AdminCreateDuplicateProductTest"> + <annotations> + <stories value="Create Product"/> + <title value="Create Duplicate Product With Existed Subcategory Name And UrlKey"/> + <description value="Login as admin and create duplicate Product"/> + <testCaseId value="MC-14714"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SubCategory" stepKey="category"/> + <createData entity="Two_nested_categories" stepKey="subCategory"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new simple product page --> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="goToCreateProductPage"/> + + <!-- Fill the main fields in the form --> + <actionGroup ref="FillMainProductFormByString" stepKey="fillMainProductForm"> + <argument name="productName" value="$$subCategory.name$$"/> + <argument name="productSku" value="{{defaultSimpleProduct.sku}}"/> + <argument name="productPrice" value="{{defaultSimpleProduct.price}}"/> + <argument name="productQuantity" value="{{defaultSimpleProduct.quantity}}"/> + <argument name="productStatus" value="{{defaultSimpleProduct.status}}"/> + <argument name="productWeight" value="{{defaultSimpleProduct.weight}}"/> + </actionGroup> + + <!-- Select the category that we created in the before block --> + <actionGroup ref="SetCategoryByName" stepKey="setCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + + <!-- Set the url key to match the subcategory created in the before block --> + <actionGroup ref="SetProductUrlKeyByString" stepKey="fillUrlKey"> + <argument name="urlKey" value="$$subCategory.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Save the product and expect to see an error message --> + <actionGroup ref="SaveProductFormNoSuccessCheck" stepKey="tryToSaveProduct"/> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml new file mode 100644 index 0000000000000..21b3dba7140c0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.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="AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest"> + <annotations> + <stories value="Create category"/> + <title value="Flat Catalog - Update Inactive Category as Inactive, Should Not be Visible on Storefront"/> + <description value="Login as admin and create inactive flat category and update category as inactive and verify category is not visible in store front"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11009"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="CatNotActive" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--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" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select created category and make category inactive--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotActive.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <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 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage"/> + <!--Verify category is not visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation"/> + <!--Verify category is not visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml new file mode 100644 index 0000000000000..aa3dba85dfadf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateInactiveFlatCategoryTest"> + <annotations> + <stories value="Create category"/> + <title value="Flat Catalog - Create Category as Inactive, Should Not be Visible on Storefront"/> + <description value="Login as admin and create flat Inactive category and verify category is not visible in store front"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11007"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--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" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select created category and make category inactive--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableActiveCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <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 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <!--Verify category is not visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation"/> + <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage"/> + <!--Verify category is not visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondstoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation1"/> + <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml new file mode 100644 index 0000000000000..37417cd7fdb85 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -0,0 +1,91 @@ +<?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="AdminCreateInactiveInMenuFlatCategoryTest"> + <annotations> + <stories value="Create category"/> + <title value="Flat Catalog - Exclude Category from Navigation Menu"/> + <description value="Login as admin and create inactive Include In Menu flat category and verify category is not displayed in Navigation Menu"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11008"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="category"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--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" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select created category and disable Include In Menu option--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIcludeInMenuOption"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <!--Verify category is saved and Include In Menu Option is disabled in Category Page --> + <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 --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$category.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <!--Verify category is not displayed in navigation menu in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation"/> + <!--Verify category is not displayed in navigation menu in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondstoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml new file mode 100644 index 0000000000000..1f558568e9248 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -0,0 +1,87 @@ +<?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="AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Multiple Select product attribute and check its visibility in Advanced Search form"/> + <title value="Create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form"/> + <description value="Admin should be able to create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form"/> + <testCaseId value="MC-10828"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create a multiple select product attribute with two options --> + <createData entity="productAttributeMultiselectTwoOptionsNotSearchable" stepKey="attribute"/> + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="option2"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <!-- Create product attribute set --> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Filter product attribute set by attribute set name --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> + <actionGroup ref="FilterProductAttributeSetGridByAttributeSetName" stepKey="filterProductAttrSetGridByAttrSetName"> + <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> + </actionGroup> + + <!-- Assert created attribute in an unassigned attributes --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> + + <!-- Assign attribute in the group --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + + <!-- Go to Product Attribute Grid page --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> + + <!-- Change attribute property: Frontend Label --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel"/> + + <!-- Change attribute property: Use in Search >Yes --> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="seeInSearch"/> + + <!-- Change attribute property: Visible In Advanced Search >No --> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.VisibleInAdvancedSearch}}" userInput="No" stepKey="dontSeeInAdvancedSearch"/> + + <!-- Save the new product attributes --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Go to store's advanced catalog search page --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <dontSeeElement selector="{{StorefrontCatalogSearchAdvancedFormSection.AttributeByCode('$$attribute.attribute_code$$')}}" stepKey="dontSeeAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml new file mode 100644 index 0000000000000..282331924bca3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml @@ -0,0 +1,68 @@ +<?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="AdminCreateNewAttributeFromProductTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that New Attribute from Product is create"/> + <description value="Check that New Attribute from Product is create"/> + <severity value="MAJOR"/> + <testCaseId value="MC-12296"/> + <useCaseId value="MAGETWO-59055"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete create data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!--Delete store views--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!--Delete Attribute--> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create 2 store views--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!--Go to created product page and create new attribute--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" stepKey="createAttribute"> + <argument name="attributeName" value="{{productDropDownAttribute.attribute_code}}"/> + <argument name="attributeType" value="Dropdown"/> + <argument name="firstStoreViewName" value="{{customStoreEN.name}}"/> + <argument name="secondStoreViewName" value="{{customStoreFR.name}}"/> + </actionGroup> + + <!--Check attribute existence in product page attribute section--> + <conditionalClick selector="{{AdminProductAttributeSection.attributeSectionHeader}}" dependentSelector="{{AdminProductAttributeSection.attributeSection}}" visible="false" stepKey="openAttributeSection"/> + <seeElement selector="{{AdminProductAttributeSection.dropDownAttribute(productDropDownAttribute.attribute_code)}}" stepKey="seeNewAttributeInProductPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml new file mode 100644 index 0000000000000..5c798db29b976 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -0,0 +1,133 @@ +<?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="AdminCreateProductAttributeFromProductPageTest"> + <annotations> + <stories value="Create product Attribute"/> + <title value="Create Product Attribute from Product Page"/> + <description value="Login as admin and create new product attribute from product page with Text Field"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10899"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!--Create Simple Product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--<deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/>--> + <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> + <argument name="ProductAttribute" value="newProductAttribute"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!-- Select Created Product--> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> + + <!-- Create New Product Attribute --> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> + <waitForPageLoad stepKey="waitForAttributePageToLoad"/> + <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> + <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Text Field" stepKey="selectTextField"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillDefaultValue"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="scrollToAdvancedAttributeProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties1"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> + <waitForPageLoad stepKey="waitForStoreFrontToLoad"/> + <scrollTo stepKey="scroll1" selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" x="0" y="-80"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isComparable}}" stepKey="enableIsUComparableption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.allowHtmlTags}}" stepKey="enableAllowHtmlTags"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> + <waitForPageLoad stepKey="waitForProductToSave"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Verify product attribute added in product form --> + <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> + <click selector="{{AdminProductFormSection.attributeTab}}" stepKey="clickOnAttribute"/> + <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> + + <!--Verify Product Attribute in Attribute Form --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="seeAttributeCode"/> + <see selector="{{AdminProductAttributeGridSection.defaultLabelColumn}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeDefaultLabel"/> + <see selector="{{AdminProductAttributeGridSection.isVisibleColumn}}" userInput="Yes" stepKey="seeIsVisibleColumn"/> + <see selector="{{AdminProductAttributeGridSection.isSearchableColumn}}" userInput="Yes" stepKey="seeSearchableColumn"/> + <see selector="{{AdminProductAttributeGridSection.isComparableColumn}}" userInput="Yes" stepKey="seeComparableColumn"/> + + <!--Verify Product Attribute is present in Category Store Front Page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> + <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> + <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> + <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> + + <!--Verify Product Attribute present in search page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage1"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad1"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAttribute"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml new file mode 100644 index 0000000000000..d4d6496e018f5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateProductAttributeRequiredTextFieldTest"> + <annotations> + <stories value="Manage products"/> + <title value="Create Custom Product Attribute Text Field (Required) from Product Page"/> + <description value="Login as admin and create product attribute with Text Field and Required option"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10906"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!--Create Simple Product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> + <argument name="ProductAttribute" value="newProductAttribute"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!-- Select Created Product--> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> + + <!-- Create Product Attribute --> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> + <waitForPageLoad stepKey="waitForAttributePageToLoad"/> + <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> + <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Text Field" stepKey="selectTextField"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isRequired}}" stepKey="enableIsRequiredOption"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> + <selectOption selector="{{AdminCreateNewProductAttributeSection.scope}}" userInput="Global" stepKey="selectScope"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillDefaultValue"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> + <waitForPageLoad stepKey="waitForProductToSave"/> + + <!--Verify product attribute added in product form and Is Required message displayed--> + <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> + <seeElement selector="{{AdminProductFormSection.attributeFieldError}}" stepKey="seeAttributeInputFiledErrorMessage"/> + + <!--Fill the Required field and save the product --> + <fillField selector="{{AdminProductFormSection.attributeRequiredInput(newProductAttribute.attribute_code)}}" userInput="attribute" stepKey="fillTheAttributeRequiredInputField"/> + <scrollToTopOfPage stepKey="scrollToTopOfProductFormPage"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct1"/> + <waitForPageLoad stepKey="waitForProductToSave1"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml new file mode 100644 index 0000000000000..3487de656173f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml @@ -0,0 +1,61 @@ +<?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="AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest"> + <annotations> + <stories value="Create simple product"/> + <title value="Create simple product with (Country of Manufacture) Attribute SKU Mask"/> + <description value="Test log in to Create simple product and Create simple product with (Country of Manufacture) Attribute SKU Mask"/> + <testCaseId value="MC-11024"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI stepKey="setCountryOfManufacture" command="config:set catalog/fields_masks/sku" arguments="{{name}}-{{country_of_manufacture}}"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI stepKey="setName" command="config:set catalog/fields_masks/sku" arguments="{{name}}"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" /> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectSimpleProduct"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickSimpleProductFromDropDownList"/> + + <!-- Create simple product with country of manufacture attribute --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}" stepKey="fillSimpleProductName"/> + <selectOption selector="{{AdminProductFormSection.countryOfManufacture}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="selectCountryOfManufacture"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.weight}}" stepKey="fillSimpleProductWeight"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.quantity}}" stepKey="fillSimpleProductQuantity"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductToSave"/> + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search created simple product(from above step) in the grid page to verify sku masked as name and country of manufacture --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchCreatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="fillSkuFilterFieldWithNameAndCountryOfManufactureInput" /> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <waitForPageLoad stepKey="waitForProductSearchAfterApplyingFilters"/> + <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="seeSimpleProductSkuMaskedAsNameAndCountryOfManufacture"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml new file mode 100644 index 0000000000000..c3fe666c84fd4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml @@ -0,0 +1,56 @@ +<?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="AdminCreateVirtualProductFillingRequiredFieldsOnlyTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product filling required fields only"/> + <description value="Test log in to Create virtual product and Create virtual product filling required fields only"/> + <testCaseId value="MC-6031"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with required fields only --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithRequiredFields.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved" /> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify we see created virtual product(from the above step) on the product grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickSelector"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName1"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <seeInField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="seeVirtualProductName"/> + <seeInField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="seeVirtualProductSku"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml new file mode 100644 index 0000000000000..26ad7a46a73d7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml @@ -0,0 +1,97 @@ +<?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="AdminCreateVirtualProductOutOfStockWithTierPriceTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product out of stock with tier price"/> + <description value="Test log in to Create virtual product and Create virtual product out of stock with tier price"/> + <testCaseId value="MC-6036"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product out of stock with tier price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductOutOfStock.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnDefault.website_0}}" stepKey="selectProductTierPriceWebsite"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnDefault.customer_group_0}}" stepKey="selectProductTierPriceCustGroup"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnDefault.qty_0}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnDefault.price_0}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButtonToAddAnotherRow"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('1')}}" userInput="{{tierPriceOnDefault.website_1}}" stepKey="clickProductTierPriceWebsite1"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('1')}}" userInput="{{tierPriceOnDefault.customer_group_1}}" stepKey="clickProductTierPriceCustGroup1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('1')}}" userInput="{{tierPriceOnDefault.qty_1}}" stepKey="fillProductTierPriceQuantityInput1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('1')}}" userInput="{{tierPriceOnDefault.price_1}}" stepKey="selectProductTierPriceFixedPrice1"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductOutOfStock.quantity}}" stepKey="fillVirtualProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{virtualProductOutOfStock.status}}" stepKey="selectStockStatusOutOfStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductOutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify we see created virtual product out of stock with tier price on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(virtualProductOutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', tierPriceOnDefault.qty_0)}}" stepKey="firstTierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage1"> + <expectedResult type="string">Buy {{tierPriceOnDefault.qty_0}} for ${{tierPriceOnDefault.price_0}} each and save 100%</expectedResult> + <actualResult type="variable">firstTierPriceText</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('2', tierPriceOnDefault.qty_1)}}" stepKey="secondTierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage2"> + <expectedResult type="string">Buy {{tierPriceOnDefault.qty_1}} for ${{tierPriceOnDefault.price_1}} each and save 100%</expectedResult> + <actualResult type="variable">secondTierPriceText</actualResult> + </assertEquals> + + <!-- Verify customer see product out of stock status on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{virtualProductOutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductOutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml new file mode 100644 index 0000000000000..17769c79677f7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -0,0 +1,160 @@ +<?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="AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product with custom options suite and import options"/> + <description value="Test log in to Create virtual product and Create virtual product with custom options suite and import options"/> + <testCaseId value="MC-6034"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with custom options suite and import options --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductCustomImportOptions.price}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductCustomImportOptions.quantity}}" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormSection.productStockStatus}}" stepKey="clickProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductCustomImportOptions.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> + + <!-- Create virtual product with customizable options dataSet1 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> + <waitForPageLoad stepKey="waitForFirstOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="fillOptionTitleForFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="fillOptionPriceForFirstDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="fillOptionSkuForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForFirstDataSet"/> + + <!-- Create virtual product with customizable options dataSet2 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSet"/> + <waitForPageLoad stepKey="waitForSecondDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="fillOptionTitleForSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="fillOptionPriceForSecondDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="fillOptionSkuForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForSecondDataSet"/> + + <!-- Create virtual product with customizable options dataSet3 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForThirdSetOfData"/> + <waitForPageLoad stepKey="waitForThirdSetOfDataToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="fillOptionTitleForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForThirdDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="fillOptionTitleForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="fillOptionPriceForThirdDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="fillOptionSkuForThirdDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="fillOptionTitleForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="fillOptionPriceForThirdDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="fillOptionSkuForThirdDataSetSecondRow"/> + + <!-- Create virtual product with customizable options dataSet4 --> + <scrollToTopOfPage stepKey="scrollToAddOptionButton"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForFourthDataSet"/> + <waitForPageLoad stepKey="waitForFourthDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForFourthSetOfData"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForFourthDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="fillOptionTitleForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="fillOptionPriceForFourthDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="fillOptionSkuForFourthDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="fillOptionTitleForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="fillOptionPriceForFourthDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductName"/> + <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> + + <!-- Verify we see created virtual product with custom options suite and import options on the storefront page --> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{virtualProductCustomImportOptions.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductCustomImportOptions.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify we see customizable options are Required --> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> + + <!--Verify we see customizable option titles and prices --> + <grabMultiple selector="{{StorefrontProductInfoMainSection.allCustomOptionLabels}}" stepKey="allCustomOptionLabels" /> + <assertEquals stepKey="verifyLabels"> + <actualResult type="variable">allCustomOptionLabels</actualResult> + <expectedResult type="array">[{{virtualProductCustomizableOption1.title}} + ${{virtualProductCustomizableOption1.option_0_price}}, {{virtualProductCustomizableOption2.title}} + ${{virtualProductCustomizableOption2.option_0_price}}, {{virtualProductCustomizableOption3.title}}, {{virtualProductCustomizableOption4.title}}]</expectedResult> + </assertEquals> + <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(virtualProductCustomizableOption4.title)}}" stepKey="fourthOptionId" /> + <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$fourthOptionId})}}" stepKey="grabFourthOptions" /> + <assertEquals stepKey="assertFourthSelectOptions"> + <actualResult type="variable">grabFourthOptions</actualResult> + <expectedResult type="array">['-- Please Select --', {{virtualProductCustomizableOption4.option_0_title}} +$900.90, {{virtualProductCustomizableOption4.option_1_title}} +$20.02]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml new file mode 100644 index 0000000000000..78247f4943596 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -0,0 +1,132 @@ +<?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="AdminCreateVirtualProductWithTierPriceForGeneralGroupTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product with tier price for General group"/> + <description value="Test log in to Create virtual product and Create virtual product with tier price for General group"/> + <testCaseId value="MC-6033"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + <createData entity="Simple_US_CA_Customer" stepKey="customer" /> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with tier price for general group --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductGeneralGroup.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductGeneralGroup.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.website}}" stepKey="selectProductTierPriceWebsite"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.customer_group}}" stepKey="selectProductTierPriceGroup"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnGeneralGroup.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnGeneralGroup.price}}" stepKey="fillProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductGeneralGroup.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductGeneralGroup.quantity}}" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormSection.productStockStatus}}" stepKey="clickProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductGeneralGroup.urlKey}}" stepKey="fillUrlKeyInput"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillVirtualProductName"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Verify we see created virtual product with tier price for general group(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductGeneralGroup.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductGeneralGroup.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.website}}" stepKey="seeProductTierPriceWebsite"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.customer_group}}" stepKey="seeProductTierPriceGroup"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnGeneralGroup.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnGeneralGroup.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductGeneralGroup.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductGeneralGroup.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{virtualProductGeneralGroup.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductGeneralGroup.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see created virtual product on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see created virtual product with tier price for general group(from above step) in storefront page with customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$customer$$" /> + </actionGroup> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillVirtualProductNameInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeVirtualProductName"/> + <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> + + <!-- Verify customer see created virtual product with tier price --> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnGeneralGroup.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> + </assertEquals> + <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductPageToBeLoaded"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnGeneralGroup.qty}} for ${{tierPriceOnGeneralGroup.price}} each and save 20%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml new file mode 100644 index 0000000000000..6ef2569945fa6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -0,0 +1,123 @@ +<?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="AdminCreateVirtualProductWithTierPriceTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product with tier price"/> + <description value="Test log in to Create virtual product and Create virtual product with tier price"/> + <testCaseId value="MC-6032"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with tier price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductBigQty.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductBigQty.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductBigQty.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductBigQty.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{virtualProductBigQty.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName1"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened" /> + + <!-- Verify we see created virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductBigQty.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductBigQty.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductBigQty.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductBigQty.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{virtualProductBigQty.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see created virtual product on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see created virtual product with tier price(from above step) on storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductBigQty.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductName"/> + <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> + </assertEquals> + <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductPageToBeLoaded" /> + + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml new file mode 100644 index 0000000000000..cb41b0292d33a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml @@ -0,0 +1,88 @@ +<?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="AdminCreateVirtualProductWithoutManageStockTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product without manage stock"/> + <description value="Test log in to Create virtual product and Create virtual product without manage stock"/> + <testCaseId value="MC-6035"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product without manage stock --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithoutManageStock.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{virtualProductWithoutManageStock.special_price}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductWithoutManageStock.quantity}}" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> + <click selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" stepKey="clickManageStock"/> + <checkOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="CheckUseConfigSettingsCheckBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductWithoutManageStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify customer see created virtual product without manage stock on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(virtualProductWithoutManageStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="seeVirtualProductSku"/> + + <!-- Verify customer see product special price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductWithoutManageStock.special_price}}</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + + <!-- Verify customer see product old price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductWithoutManageStock.price}}</expectedResult> + <actualResult type="variable">oldPriceAmount</actualResult> + </assertEquals> + + <!-- Verify customer see product in stock status on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{virtualProductWithoutManageStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml new file mode 100644 index 0000000000000..4d28ccbd44d2c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.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="AdminDeleteAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <title value="Delete Attribute Set"/> + <description value="Admin should be able to delete an attribute set"/> + <testCaseId value="MC-4413"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="SimpleProductWithCustomAttributeSet" stepKey="SimpleProductWithCustomAttributeSet"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createAttributeSet"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetsPage"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="filterByAttributeName"/> + <!-- Filter the grid to find created below attribute set --> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <!-- Delete attribute set and confirm the modal --> + <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> + <click selector="{{AdminProductAttributeSetSection.modalOk}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="The attribute set has been removed." stepKey="deleteMessage"/> + <!-- Assert the attribute set is not in the grid --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="filterByAttributeName2"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch2"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <!-- Search for the product by sku and name on the product page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndex"/> + <waitForPageLoad stepKey="waitForAdminProductIndex"/> + <actionGroup ref="filterProductGridBySkuAndName" stepKey="filerProductsBySkuAndName"> + <argument name="product" value="SimpleProductWithCustomAttributeSet"/> + </actionGroup> + <!-- Should not see the product --> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml new file mode 100644 index 0000000000000..0df9dd0b57545 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml @@ -0,0 +1,121 @@ +<?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="AdminDeleteConfigurableChildProductsTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Configurable Product should not be visible on storefront after child products are deleted"/> + <description value="Login as admin, delete configurable child product and verify product displays out of stock in store front"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13684"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Set Display Out Of Stock Product --> + <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0 "/> + <!--Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create Default Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <!--Delete Created Data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Product in Store Front Page --> + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <!--Verify Product is visible and In Stock --> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="seeProductAttributeLabel"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="seeProductAttributeOptions"/> + <!-- Delete Child products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> + <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> + </actionGroup> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> + <argument name="sku" value="$$createConfigChildProduct2.sku$$"/> + </actionGroup> + <!--Verify product is not visible in category store front page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInStoreFrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickOnCategory"/> + <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="dontSeeProductInCategoryPage"/> + <!--Open Product Store Front Page and Verify Product is Out Of Stock --> + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="openProductInStoreFront1"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> + <dontSee selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="dontSeeProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="OUT OF STOCK" stepKey="seeProductStatusInStoreFront1"/> + <dontSee selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="dontSeeProductAttributeLabel"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="dontSeeProductAttributeOptions"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml new file mode 100644 index 0000000000000..3841c061c2629 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml @@ -0,0 +1,72 @@ +<?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="AdminDeleteDropdownProductAttributeFromAttributeSetTest"> + <annotations> + <stories value="Delete product attributes"/> + <title value="Delete Product Attribute, Dropdown Type, from Attribute Set"/> + <description value="Login as admin and delete dropdown type product attribute from attribute set"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10885"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Create Dropdown Product Attribute --> + <createData entity="productDropDownAttribute" stepKey="attribute"/> + <!-- Create Attribute set --> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + </before> + <after> + <!--Delete Created Data --> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Open Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <!-- Filter created Product Attribute Set --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="fillAttributeSetName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName($$createAttributeSet.attribute_set_name$$)}}" stepKey="clickOnAttributeSet"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> + <!--Assign Attribute to the Group and save the attribute set --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttribute"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToSave"/> + <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> + <!--Delete product attribute from product attribute grid --> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <!--Confirm Attribute is not present in Product Attribute Grid --> + <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see selector="{{AdminProductAttributeGridSection.FirstRow}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + <!-- Verify Attribute is not present in Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets1"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter1"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="fillAttributeSetName1"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName($$createAttributeSet.attribute_set_name$$)}}" stepKey="clickOnAttributeSet1"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad1"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml new file mode 100644 index 0000000000000..54b83e034fb11 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml @@ -0,0 +1,64 @@ +<?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="DeleteProductAttributeTest"> + <annotations> + <features value="Catalog"/> + <title value="Delete Product Attribute"/> + <description value="Admin should able to delete a product attribute"/> + <testCaseId value="MC-10887"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <!-- Assert the product attribute is not in the grid by Attribute code --> + <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterByAttributeCode"> + <argument name="ProductAttributeCode" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <!--Assert the product attribute is not in the grid by Default Label --> + <actionGroup ref="filterProductAttributeByDefaultLabel" stepKey="filterByDefaultLabel"> + <argument name="productAttributeLabel" value="$$createProductAttribute.default_frontend_label$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> + <!--Go to the Catalog > Products page and create Simple Product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductBtn"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="chooseAddSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductAdded"/> + <!-- Press Add Attribute button --> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForAttributeAdded"/> + <!-- Filter By Attribute Label on Add Attribute Page --> + <click selector="{{AdminProductFiltersSection.filter}}" stepKey="clickOnFilter"/> + <actionGroup ref="filterProductAttributeByAttributeLabel" stepKey="filterByAttributeLabel"> + <argument name="productAttributeLabel" value="$$createProductAttribute.default_frontend_label$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage3"/> + <!-- Filter By Attribute Code on Export > Products page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="navigateToSystemExport"/> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> + <click selector="{{AdminExportAttributeSection.resetFilter}}" stepKey="resetFilter"/> + <fillField selector="{{AdminExportAttributeSection.filterByAttributeCode}}" userInput="$$createProductAttribute.attribute_code$$" stepKey="setAttributeCode"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminExportAttributeSection.search}}" stepKey="searchForAttribute"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml new file mode 100644 index 0000000000000..7f6a1333b721a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.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="AdminDeleteProductWithCustomOptionTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete products"/> + <title value="Delete Product with Custom Option"/> + <description value="Admin should be able to delete a product with custom option"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11015"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <updateData createDataKey="createSimpleProduct" entity="productWithOptions2" stepKey="updateProductWithCustomOption"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProductFilteredBySkuAndName"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on product page --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml new file mode 100644 index 0000000000000..e4b269dff96ba --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -0,0 +1,51 @@ +<?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="AdminDeleteRootCategoryAssignedToStoreTest"> + <annotations> + <stories value="Delete categories"/> + <title value="Cannot delete root category assigned to some store"/> + <description value="Login as admin and root category can not be deleted when category is assigned with any store."/> + <testCaseId value="MC-6050"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory" /> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> + <argument name="storeGroupName" value="customStore.code"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <see userInput="You saved the store." stepKey="seeSaveMessage"/> + + <!--Verify Delete Root Category can not be deleted--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded1"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name))}}" stepKey="clickRootCategoryInTree"/> + + <!--Verify Delete button is not displayed--> + <dontSeeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="dontSeeDeleteButton"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml new file mode 100644 index 0000000000000..e7ab14c77945a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -0,0 +1,44 @@ +<?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="AdminDeleteRootCategoryTest"> + <annotations> + <stories value="Delete categories"/> + <title value="Can delete a root category not assigned to any store"/> + <description value="Login as admin and delete a root category not assigned to any store"/> + <testCaseId value="MC-6048"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory" /> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Verify Created root Category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> + + <!--Delete Root Category--> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + + <!--Verify Root Category is not listed in backend--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> + <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml new file mode 100644 index 0000000000000..6df571f403ac9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.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="AdminDeleteRootSubCategoryTest"> + <annotations> + <stories value="Delete categories"/> + <title value="Can delete a subcategory"/> + <description value="Login as admin and delete a root sub category"/> + <testCaseId value="MC-6049"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory" /> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> + <argument name="storeGroupName" value="customStore.code"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create a Store--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <see userInput="You saved the store." stepKey="seeSaveMessage"/> + + <!--Create a Store View--> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> + <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> + <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> + + <!--Go To store front page--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + + <!--Verify subcategory displayed in store front--> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> + + <!--Delete SubCategory--> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + + <!--Verify Sub Category is absent in backend --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> + + <!--Verify Sub Category is not present in Store Front--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> + + <!--Verify in Category is not in Url Rewrite grid--> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <see selector="{{AdminUrlRewriteIndexSection.emptyRecordMessage}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml new file mode 100644 index 0000000000000..7c460a3dfc51e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml @@ -0,0 +1,53 @@ +<?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="AdminDeleteSimpleProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete products"/> + <title value="Delete Simple Product"/> + <description value="Admin should be able to delete a simple product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11013"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProductFilteredBySkuAndName"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml new file mode 100644 index 0000000000000..6de1a5cd359cd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml @@ -0,0 +1,34 @@ +<?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="AdminDeleteSystemProductAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete System Product Attribute"/> + <title value="Delete System Product Attribute"/> + <description value="Admin should not be able to see Delete Attribute button"/> + <testCaseId value="MC-10893"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newsFromDate.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <dontSeeElement selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="dontSeeDeleteAttributeBtn" /> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml new file mode 100644 index 0000000000000..c3cafb17c5eac --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml @@ -0,0 +1,88 @@ +<?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="AdminDeleteTextFieldProductAttributeFromAttributeSetTest"> + <annotations> + <stories value="Delete product attributes"/> + <title value="Delete Product Attribute, Text Field, from Attribute Set"/> + <description value="Login as admin and delete Text Field type product attribute from attribute set"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10886"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Create Product Attribute and assign to Default Product Attribute Set --> + <createData entity="newProductAttribute" stepKey="attribute"/> + <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> + <requiredEntity createDataKey="attribute"/> + </createData> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + </before> + <after> + <!--Delete cteated Data --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimplaeProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Open Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <!--Select Default Product Attribute Set --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> + <see selector="{{AdminProductAttributeSetEditSection.groupTree}}" userInput="$$attribute.attribute_code$$" stepKey="seeAttributeInAttributeGroupTree"/> + <!--Open Product Index Page and filter the product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct2"/> + </actionGroup> + <!--Verify Created Product Attribute displayed in Product page --> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeElement selector="{{AdminProductFormSection.newAddedAttribute($$attribute.attribute_code$$)}}" stepKey="seeProductAttributeIsAdded"/> + <!--Delete product attribute from product attribute grid --> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <!-- Confirm attribute is not present in product attribute grid --> + <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see stepKey="seeEmptyRow" selector="{{AdminProductAttributeGridSection.FirstRow}}" userInput="We couldn't find any records."/> + <!-- Verify Attribute is not present in Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets1"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter1"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName1"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad1"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> + <!--Verify Product Attribute is not present in Product Index Page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductIndexPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad1"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct1"> + <argument name="product" value="SimpleProduct2"/> + </actionGroup> + <!--Verify Product Attribute is not present in Product page --> + <click selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}" stepKey="openSelectedProduct1"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <dontSeeElement selector="{{AdminProductFormSection.newAddedAttribute($$attribute.attribute_code$$)}}" stepKey="dontSeeProductAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml new file mode 100644 index 0000000000000..413d53d1c3746 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.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="AdminDeleteVirtualProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete products"/> + <title value="Delete Virtual Product"/> + <description value="Admin should be able to delete a virtual product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11014"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="defaultVirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProductFilteredBySkuAndName"> + <argument name="product" value="$$createVirtualProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on product page --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="amOnVirtualProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createVirtualProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createVirtualProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createVirtualProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml new file mode 100644 index 0000000000000..f3ec225540c75 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.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="AdminFilterByNameByStoreViewOnProductGridTest"> + <annotations> + <features value="Catalog"/> + <stories value="Filter products"/> + <title value="Product grid filtering by store view level attribute"/> + <description value="Verify that products grid can be filtered on all store view level by attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98755"/> + <useCaseId value="MAGETWO-98335"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> + <argument name="storeView" value="_defaultStore.name"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="filterProductGridByName" stepKey="filterGridByName"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{SimpleProduct2.name}}" stepKey="seeProductNameInGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml index e0e214342ad72..5c434ecabf80d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminFilteringCategoryProductsUsingScopeSelectorTest"> <annotations> - <features value="Catalog"/> + <stories value="Filtering Category Products"/> <title value="Filtering Category Products using scope selector"/> <description value="Filtering Category Products using scope selector"/> <severity value="MAJOR"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml new file mode 100644 index 0000000000000..b24ed7f9c9a81 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -0,0 +1,69 @@ +<?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="AdminGridPageNumberAfterSaveAndCloseActionTest"> + <annotations> + <features value="Catalog"/> + <title value="Checking Catalog grid page number after Save and Close action"/> + <description value="Checking Catalog grid page number after Save and Close action"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96164"/> + <useCaseId value="MAGETWO-96127"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Clear product grid--> + <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteProductIfTheyExist"/> + <createData stepKey="category1" entity="SimpleSubCategory"/> + <createData stepKey="product1" entity="SimpleProduct"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData stepKey="category2" entity="SimpleSubCategory"/> + <createData stepKey="product2" entity="SimpleProduct"> + <requiredEntity createDataKey="category2"/> + </createData> + </before> + <after> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <click selector="{{AdminDataGridPaginationSection.previousPage}}" stepKey="clickPrevPageOrderGrid"/> + <actionGroup ref="adminDataGridDeleteCustomPerPage" stepKey="deleteCustomAddedPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + <deleteData stepKey="deleteCategory1" createDataKey="category1"/> + <deleteData stepKey="deleteProduct1" createDataKey="product1"/> + <deleteData stepKey="deleteCategory2" createDataKey="category2"/> + <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="adminDataGridSelectCustomPerPage" stepKey="select1OrderPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + <!--Go to the next page and edit the product--> + <comment userInput="Go to the next page and edit the product" stepKey="commentEdiProduct"/> + <click selector="{{AdminDataGridPaginationSection.nextPage}}" stepKey="clickNextPageOrderGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndClose" stepKey="saveAndCloseProduct"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="2" stepKey="seeOnSecondPageOrderGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml new file mode 100644 index 0000000000000..79ff7bcade77b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -0,0 +1,82 @@ +<?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="AdminImportCustomizableOptionToProductWithSKUTest"> + <annotations> + <features value="Catalog"/> + <title value="Import customizable options to a product with existing SKU"/> + <description value="Import customizable options to a product with existing SKU"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98211"/> + <useCaseId value="MAGETWO-70232"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <comment userInput="Create category" stepKey="commentCreateCategory"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create two product --> + <comment userInput="Create two product" stepKey="commentCreateTwoProduct"/> + <createData entity="SimpleProduct2" stepKey="createFirstProduct"/> + <createData entity="ApiSimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete second product with changed sku--> + <comment userInput="Delete second product with changed sku" stepKey="commentDeleteProduct"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondProduct"> + <argument name="sku" value="$$createFirstProduct.sku$$-1"/> + </actionGroup> + <!--Delete created data--> + <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Go to product page --> + <comment userInput="Go to product page" stepKey="commentGoToProductPage"/> + <amOnPage url="{{AdminProductEditPage.url($$createFirstProduct.id$$)}}" stepKey="goToProductEditPage"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad"/> + <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption1"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption2"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Change second product sku to first product sku--> + <comment userInput="Change second product sku to first product sku" stepKey="commentChangeSecondProduct"/> + <amOnPage url="{{AdminProductEditPage.url($$createSecondProduct.id$$)}}" stepKey="goToProductEditPage1"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad1"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="$$createFirstProduct.sku$$" stepKey="fillProductSku1"/> + <!--Import customizable options and check--> + <comment userInput="Import customizable options and check" stepKey="commentImportOptions"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <actionGroup ref="importProductCustomizableOptions" stepKey="importOptions"> + <argument name="productName" value="$$createFirstProduct.name$$"/> + </actionGroup> + <actionGroup ref="checkCustomizableOptionImport" stepKey="checkFirstOptionImport"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="checkCustomizableOptionImport" stepKey="checkSecondOptionImport"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + <!--Save product and check sku changed message--> + <comment userInput="Save product and check sku changed message" stepKey="commentSAveProductAndCheck"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + <see userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml new file mode 100644 index 0000000000000..4d581bae700d7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -0,0 +1,79 @@ +<?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="AdminMassProductPriceUpdateTest"> + <annotations> + <stories value="Mass product update "/> + <features value="Catalog"/> + <title value="Mass update simple product price"/> + <description value="Login as admin and update mass product price"/> + <testCaseId value="MC-8510"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct1"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct2"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!--Search products using keyword --> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="Testp"/> + </actionGroup> + + <!--Sort Products by ID in descending order--> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + + <!--Select products--> + <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku($$simpleProduct1.sku$$)}}" stepKey="selectFirstProduct"/> + <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku($$simpleProduct2.sku$$)}}" stepKey="selectSecondProduct"/> + + <!-- Update product price--> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickChangeStatus"/> + <waitForPageLoad stepKey="waitForProductAttributePageToLoad"/> + <scrollTo stepKey="scrollToPriceCheckBox" selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" x="0" y="-160"/> + <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="selectPriceCheckBox"/> + <fillField stepKey="fillPrice" selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="90.99"/> + <click stepKey="clickOnSaveButton" selector="{{AdminEditProductAttributesSection.Save}}"/> + <waitForPageLoad stepKey="waitForUpdatedProductToSave" /> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> + + <!--Verify product name, sku and updated price--> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct1.sku$$)}}"/> + <waitForPageLoad stepKey="waitForFirstProductToLoad"/> + <seeInField stepKey="seeFirstProductNameInField" selector="{{AdminProductFormSection.productName}}" userInput="$$simpleProduct1.name$$"/> + <seeInField stepKey="seeFirstProductSkuInField" selector="{{AdminProductFormSection.productSku}}" userInput="$$simpleProduct1.sku$$"/> + <seeInField stepKey="seeFirstProductPriceInField" selector="{{AdminProductFormSection.productPrice}}" userInput="90.99"/> + <click stepKey="clickOnBackButton" selector="{{AdminGridMainControls.back}}"/> + <waitForPageLoad stepKey="waitForProductsToLoad"/> + <click stepKey="openSecondProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct2.sku$$)}}"/> + <waitForPageLoad stepKey="waitForSecondProductToLoad"/> + <seeInField stepKey="seeSecondProductNameInField" selector="{{AdminProductFormSection.productName}}" userInput="$$simpleProduct2.name$$"/> + <seeInField stepKey="seeSecondProductSkuInField" selector="{{AdminProductFormSection.productSku}}" userInput="$$simpleProduct2.sku$$"/> + <seeInField stepKey="seeSecondProductPriceInField" selector="{{AdminProductFormSection.productPrice}}" userInput="90.99"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index c0eebd1512d6d..8a44c8093ca5e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -58,7 +58,13 @@ <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$$createProductOne.price$$0" stepKey="fillAttributeNameField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index 845c47c0e4c20..bee13bec370da 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -52,7 +52,13 @@ <click selector="{{AdminEditProductAttributesSection.ChangeAttributeDescriptionToggle}}" stepKey="toggleToChangeDescription"/> <fillField selector="{{AdminEditProductAttributesSection.AttributeDescription}}" userInput="Updated $$createProductOne.custom_attributes[description]$$" stepKey="fillAttributeDescriptionField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml new file mode 100644 index 0000000000000..247711295a555 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -0,0 +1,123 @@ +<?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="AdminMoveAnchoredCategoryToDefaultCategoryTest"> + <annotations> + <stories value="Move categories"/> + <title value="Move default anchored subcategory with anchored parent to default subcategory"/> + <description value="Login as admin,move anchored subcategory with anchored parent to default subcategory"/> + <testCaseId value="MC-6493"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <features value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Enable Anchor for _defaultCategory Category--> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!--Enable Anchor for FirstLevelSubCat Category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="addSubCategoryName"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting1"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting1"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> + + <!--Enable Anchor for SimpleSubCategory Category and add products to the Category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting2"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting2"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor2"/> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory1"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> + + <!--Open Category in store front page--> + <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> + + <!--<Verify breadcrumbs in store front page--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{FirstLevelSubCat.name}}, {{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + + <!--Verify Product displayed in category store front page--> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <!--Move SubCategory under Default Category--> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad1"/> + + <!--Verify breadcrumbs in store front page after the move--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> + <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> + <expectedResult type="array">['Home',{{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbsAfterMove</actualResult> + </assertEquals> + + <!--Open Category in store front--> + <amOnPage url="{{StorefrontCategoryPage.url(SimpleSubCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnStoreNavigationBarAfterMove"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct1"/> + <waitForPageLoad stepKey="waitForProductToLoad2"/> + + <!--Verify product name on Store Front--> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductNameAfterMove"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml new file mode 100644 index 0000000000000..ba6e6a43674c3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -0,0 +1,113 @@ +<?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="AdminMoveCategoryAndCheckUrlRewritesTest"> + <annotations> + <stories value="Move categories"/> + <title value="URL Rewrites for subcategories during creation and move"/> + <description value="Login as admin, move category from one to another and check category url rewrites"/> + <testCaseId value="MC-6494"/> + <features value="Catalog"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Create second level category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SubCategory.name}}" stepKey="addSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> + + <!--Create third level category under second level category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> + + <!--Open Url Rewrite Page--> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!--Search third level category Redirect Path, Target Path and Redirect Type--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="fillRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad0"/> + + <!--Verify Category RedirectType--> + <see stepKey="verifyTheRedirectType" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> + + <!--Verify Redirect Path --> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" stepKey="verifyTheRedirectPath"/> + + <!--Verify Category Target Path--> + <see stepKey="verifyTheTargetPath" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}"/> + + <!--Open Category Page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <!--Move the third level category under first level category --> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + + <!--Open Url Rewrite page --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage1"/> + <waitForPageLoad stepKey="waitForUrlRewritePage1"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" stepKey="fillCategoryUrlKey1"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + + <!--Verify new Redirect Path after move --> + <see stepKey="verifyTheRequestPathAfterMove" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" /> + + <!--Verify new Target Path after move --> + <see stepKey="verifyTheTargetPathAfterMove" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}" /> + + <!--Verify new RedirectType after move --> + <see stepKey="verifyTheRedirectTypeAfterMove" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> + + <!--Verify before move Redirect Path displayed with associated Target Path and Redirect Type--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="fillCategoryUrlKey2"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton2"/> + <waitForPageLoad stepKey="waitForPageToLoad5"/> + <see stepKey="verifyTheRedirectTypeAfterMove1" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" /> + <see stepKey="verifyTheRequestPathAfterMove1" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" /> + <see stepKey="verifyTheTargetPathAfterMove1" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" /> + + <!--Verify before move Redirect Path directs to the category page--> + <amOnPage url="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnStoreNavigationBar"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml new file mode 100644 index 0000000000000..d17078d794b42 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -0,0 +1,117 @@ +<?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="AdminMoveCategoryFromParentAnchoredCategoryTest"> + <annotations> + <stories value="Move categories"/> + <title value="Move default subcategory with anchored parent to default subcategory"/> + <description value="Login as admin,move subcategory with anchored parent to default subcategory"/> + <testCaseId value="MC-6492"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <features value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Enable Anchor for _defaultCategory category --> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + + <!--Create a Subcategory under _defaultCategory category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + + <!--Add a product to SimpleSubCategory category--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!--Verify category displayed in store front page--> + <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> + + <!--Check category breadcrumbs in store front page--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + + <!--Verify Product displayed in category store front page--> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <!--Move SubCategory under Default Category--> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + + <!--Open category in store front page--> + <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad1"/> + + <!--Verify breadcrumbs after the move in store front page--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> + <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> + <expectedResult type="array">['Home',{{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbsAfterMove</actualResult> + </assertEquals> + + <!--Open category store front page --> + <amOnPage url="{{StorefrontCategoryPage.url(SimpleSubCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + + <!--Verify Category in store front--> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryOnStoreNavigationBarAfterMove"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct1"/> + <waitForPageLoad stepKey="waitForProductToLoad2"/> + + <!--Verify product name on Store Front--> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductNameAfterMove"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml new file mode 100644 index 0000000000000..9831f73e07877 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml @@ -0,0 +1,116 @@ +<?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="AdminMoveCategoryToAnotherPositionInCategoryTreeTest"> + <annotations> + <stories value="Move categories"/> + <title value="Move Category to Another Position in Category Tree"/> + <description value="Test log in to Move Category and Move Category to Another Position in Category Tree"/> + <testCaseId value="MC-13612"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="DeleteCategory" stepKey="SecondLevelSubCat"> + <argument name="categoryEntity" value="SecondLevelSubCat"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category Page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <!-- Create three level deep sub Category --> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstLevelSubCategory"/> + <waitForPageLoad stepKey="waitForFirstLevelCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButtonAgain"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondLevelSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondLevelSubCategory"/> + <waitForPageLoad stepKey="waitForSecondLevelCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> + + <!-- Move Category to another position in category tree, but click cancel button --> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.cancel}}" stepKey="clickCancelButtonOnWarningPopup"/> + <!-- Verify Category in store front page after clicking cancel button --> + <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> + <!-- Verify breadcrumbs in store front page after clicking cancel button --> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{FirstLevelSubCat.name}},{{SecondLevelSubCat.name}}]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + + <!-- Move Category to another position in category tree and click ok button--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openTheAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitTillPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="DragCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessageForOneMoreTime"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitTheForPageToLoad"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeCategoryNameInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> + <!-- Verify Category in store front after moving category to another position in category tree --> + <amOnPage url="{{StorefrontCategoryPage.url(SecondLevelSubCat.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SecondLevelSubCat.name)}}" stepKey="seeCategoryInTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SecondLevelSubCat.name)}}" stepKey="seeCategoryOnStoreNavigationBarAfterMove"/> + <!-- Verify breadcrumbs in store front page after moving category to another position in category tree --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SecondLevelSubCat.name)}}" stepKey="clickCategoryOnNavigation"/> + <waitForPageLoad stepKey="waitForCategoryLoad"/> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> + <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> + <expectedResult type="array">['Home',{{SecondLevelSubCat.name}}]</expectedResult> + <actualResult type="variable">breadcrumbsAfterMove</actualResult> + </assertEquals> + + <!-- Open Url Rewrite page and see the url rewrite for the moved category --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePageLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="fillCategoryUrlKey"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForUrlPageToLoad"/> + <!-- Verify new Redirect Path after move --> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('2')}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheRequestPathAfterMove"/> + <!-- Verify new Target Path after move --> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('2')}}" userInput="catalog/category/view/id/{$categoryId}" stepKey="verifyTheTargetPathAfterMove"/> + <!-- Verify new RedirectType after move --> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('2')}}" userInput="No" stepKey="verifyTheRedirectTypeAfterMove"/> + <!-- Verify before move Redirect Path displayed with associated Target Path and Redirect Type--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SecondLevelSubCat.name_lwr}}" stepKey="fillTheCategoryUrlKey"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton2"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" stepKey="verifyTheRedirectTypeBeforeMove"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{FirstLevelSubCat.name_lwr}}/{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheRequestPathBeforeMove"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheTargetPathBeforeMove"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml new file mode 100644 index 0000000000000..bcd4ca8531203 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml @@ -0,0 +1,183 @@ +<?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="AdminNavigateMultipleUpSellProductsTest"> + <annotations> + <stories value="Up Sell products"/> + <title value="Promote Multiple Products (Simple, Configurable) as Up-Sell Products"/> + <description value="Login as admin and add simple and configurable Products as Up-Sell products"/> + <testCaseId value="MC-8902"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!--Create Simple Products--> + <createData entity="SimpleSubCategory" stepKey="createCategory1"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory1"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="createCategory2"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory2"/> + </createData> + + <!-- Create the configurable product with product Attribute options--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="delete"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <!--Logout as admin--> + <actionGroup ref="logout" stepKey="logout"/> + + <!--Delete created data--> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCategory1" stepKey="deleteSubCategory1"/> + <deleteData createDataKey="createCategory2" stepKey="deleteCategory2"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deletecreateConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deletecreateConfigChildProduct1"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!--Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!--Select SimpleProduct --> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Add SimpleProduct1 and ConfigProduct as Up sell products--> + <click stepKey="clickOnRelatedProducts" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> + <click stepKey="clickOnAddUpSellProducts" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProduct"> + <argument name="sku" value="$$createSimpleProduct1.sku$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForTheProductToLoad"/> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectTheSimpleProduct2"/> + <click stepKey="addSelectedProduct" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> + <waitForPageLoad stepKey="waitForProductToBeAdded"/> + <click stepKey="clickOnAddUpSellProductsButton" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterConfigurableProduct"> + <argument name="sku" value="$$createConfigProduct.sku$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForTheConfigProductToLoad"/> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectTheConfigProduct"/> + <click stepKey="addSelectedProductButton" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> + <waitForPageLoad stepKey="waitForConfigProductToBeAdded"/> + <click stepKey="clickOnRelatedProducts1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> + <click stepKey="clickOnSaveButton" selector="{{AdminProductFormActionSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForLoading1"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Go to Product Index Page --> + <click stepKey="clickOnBackButton" selector="{{AdminGridMainControls.back}}"/> + <waitForPageLoad stepKey="waitForProductsToBeLoaded"/> + + <!--Select Configurable Product--> + <actionGroup ref="filterProductGridBySku" stepKey="findConfigProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <click stepKey="openConfigProduct" selector="{{AdminProductGridSection.productRowBySku($$createConfigProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForConfigProductToLoad"/> + + <!--Add SimpleProduct1 as Up Sell Product--> + <click stepKey="clickOnRelatedProductsHeader" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> + <click stepKey="clickOnAddUpSellProductsButton1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterSimpleProduct2"> + <argument name="sku" value="$$createSimpleProduct1.sku$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForTheSimpleProduct2ToBeLoaded"/> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectSimpleProduct1"/> + <click stepKey="addSimpleProduct2" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> + <waitForPageLoad stepKey="waitForSimpleProductToBeAdded"/> + <scrollTo selector="{{AdminProductFormActionSection.saveButton}}" stepKey="scrollToTheSaveButton"/> + <click stepKey="clickOnSaveButton1" selector="{{AdminProductFormActionSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForLoading2"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown1"/> + <waitForPageLoad stepKey="waitForUpdatesTobeSaved1"/> + + <!--Go to SimpleProduct store front page--> + <amOnPage url="$$createSimpleProduct.sku$$.html" stepKey="goToSimpleProductFrontPage"/> + <waitForPageLoad stepKey="waitForProduct"/> + <see stepKey="seeProductName" userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontProductInfoMainSection.productName}}"/> + <scrollTo stepKey="scrollToTheUpSellHeading" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}"/> + + <!--Verify Up Sell Products displayed in SimpleProduct page--> + <see stepKey="seeTheUpSellHeading" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> + <see stepKey="seeSimpleProduct1" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createSimpleProduct1.name$$"/> + <see stepKey="seeConfigProduct" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createConfigProduct.name$$"/> + + <!--Go to Config Product store front page--> + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="goToConfigProductFrontPage"/> + <waitForPageLoad stepKey="waitForConfigProductToBeLoaded"/> + <scrollTo stepKey="scrollToTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}"/> + + <!--Verify Up Sell Products displayed in ConfigProduct page--> + <see stepKey="seeTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> + <see stepKey="seeSimpleProduct2" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createSimpleProduct1.name$$"/> + + <!--Go to SimpleProduct1 store front page--> + <amOnPage url="$$createSimpleProduct1.sku$$.html" stepKey="goToSimpleProduct1FrontPage"/> + <waitForPageLoad stepKey="waitForSimpleProduct1ToBeLoaded"/> + + <!--Verify No Up Sell Products displayed in SimplProduct1 page--> + <dontSee stepKey="dontSeeTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml index b9a5a31ad2168..8149bc34087fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml @@ -18,6 +18,9 @@ <testCaseId value="MAGETWO-58718"/> <group value="product"/> <group value="WYSIWYGDisabled"/> + <skip> + <issueId value="MC-13841"/> + </skip> </annotations> <before> <!-- Login Admin --> @@ -106,6 +109,7 @@ <!-- Go to Product Page and see Default Store View--> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToDefaultStorefrontProductPage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(TestImageNew.filename)}}" time="30" stepKey="waitImageToBeLoaded"/> <seeElement selector="{{StorefrontProductMediaSection.productImageActive(TestImageNew.filename)}}" stepKey="seeActiveImageDefault"/> <!-- English Switch Store View and see English Store View --> @@ -117,6 +121,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc(ProductImage.fileName)}}" stepKey="seeThumb"/> <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage"/> <waitForPageLoad time="30" stepKey="waitForProductPage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(ProductImage.filename)}}" time="30" stepKey="waitImageToBeLoaded2"/> <seeElement selector="{{StorefrontProductMediaSection.productImageActive(ProductImage.filename)}}" stepKey="seeActiveImageEnglish"/> <!-- Switch France Store View and see France Store View --> @@ -128,6 +133,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc(Magento3.fileName)}}" stepKey="seeThumb1"/> <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> <waitForPageLoad time="30" stepKey="waitForProductPage1"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(Magento3.filename)}}" time="30" stepKey="waitImageToBeLoaded3"/> <seeElement selector="{{StorefrontProductMediaSection.productImageActive(Magento3.filename)}}" stepKey="seeActiveImageFrance"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml index 1bd218d18c27d..876eedb9347c7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml index e6d3978cad7bb..8b3b38d0ece31 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoVirtualProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index 060720ab007eb..8316f54c15a52 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -65,6 +65,8 @@ </actionGroup> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml new file mode 100644 index 0000000000000..234a7c69913c9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.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="AdminSortingByWebsitesTest"> + <annotations> + <stories value="View sorting by websites"/> + <title value="Sorting by websites in Admin"/> + <description value="Sorting products by websites in Admin"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="productAssignedToCustomWebsite"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="productAssignedToMainWebsite"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create new website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> + <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnableWebUrlOptions"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="productAssignedToCustomWebsite" stepKey="deleteProductAssignedToCustomWebsite"/> + <deleteData createDataKey="productAssignedToMainWebsite" stepKey="deleteProductAssignedToMainWebsite"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Assign Custom Website to Simple Product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad stepKey="waitForCatalogProductGrid"/> + + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="assignCustomWebsiteToProduct"> + <argument name="product" value="$$productAssignedToCustomWebsite$$"/> + </actionGroup> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandSection"/> + <waitForPageLoad stepKey="waitForPageOpened"/> + <uncheckOption selector="{{ProductInWebsitesSection.website(_defaultWebsite.name)}}" stepKey="deselectMainWebsite"/> + <checkOption selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectWebsite"/> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForLoadingMaskToDisappear stepKey="waitForProductPageToSaveAgain"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> + + <!--Navigate To Product Grid To Check Website Sorting--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGridToSortByWebsite"/> + <waitForPageLoad stepKey="waitForCatalogProductGridLoaded"/> + + <!--Sorting works (By Websites) ASC--> + <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortAsc"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Main Website" stepKey="checkIfProduct1WebsitesAsc"/> + + <!--Sorting works (By Websites) DESC--> + <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortDesc"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="{{customWebsite.name}}" stepKey="checkIfProduct1WebsitesDesc"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml new file mode 100644 index 0000000000000..ed29c281b804c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresAttributeSetNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin stores attribute set navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Attribute Set"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14133"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAttributeSetPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresAttributesAttributeSet.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresAttributesAttributeSet.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml new file mode 100644 index 0000000000000..28a33c4f20c01 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresProductNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin stores product navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14132"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToProductAttributePage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresAttributesProduct.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresAttributesProduct.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml new file mode 100644 index 0000000000000..51af9d78dfd46 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml @@ -0,0 +1,94 @@ +<?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="AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that 'tier price' block not available for simple product from options without 'tier price'"/> + <description value="Check that 'tier price' block not available for simple product from options without 'tier price'"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97050"/> + <useCaseId value="MAGETWO-96842"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!--Go to storefront product page an check price box css--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectOption"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="class" stepKey="grabGrabPriceClass"/> + <assertNotContains actual="$grabGrabPriceClass" expected=".price-box .price-tier_price" expectedType="string" stepKey="assertNotEquals"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml new file mode 100644 index 0000000000000..d8d462f850f8f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.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="AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, check default URL key on the custom store view"/> + <description value="Login as admin and update category and check default URL Key on custom store view"/> + <testCaseId value="MC-6063"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Store Page --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + + <!--Create Custom Store --> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + + <!--Create Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Update Category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCateforyToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <seeInField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="seeCategoryUrlKey" userInput="{{SimpleRootSubCategory.name_lwr}}2" /> + <!--Open Category in Store Front Page--> + <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickSwitchStoreButtonOnDefaultStore"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectSecondStoreToSwitchOn"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategoryOnStoreFront"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(_defaultCategory.name)}}" stepKey="seeTheUpdatedCategoryTitle"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml new file mode 100644 index 0000000000000..479249ca678dd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -0,0 +1,58 @@ +<?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="AdminUpdateCategoryAndMakeInactiveTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, make inactive"/> + <description value="Login as admin and update category and make it Inactive"/> + <testCaseId value="MC-6060"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCreatedCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!--Update category and make category inactive--> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> + + <!--Verify Inactive Category is store front page--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> + <waitForPageLoad time="15" stepKey="wait"/> + + <!--Verify Inactive Category in category page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoaded1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="assertCategoryInTree" /> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory1"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle1" /> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="assertCategoryIsInactive"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml new file mode 100644 index 0000000000000..2cb4a6b6dd436 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -0,0 +1,82 @@ +<?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="AdminUpdateCategoryNameWithStoreViewTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, with custom store view"/> + <description value="Login as admin and update category name with custom Store View"/> + <testCaseId value="MC-6061"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open store page --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + + <!--Create Custom Store --> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + + <!--Create Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Verify created SubCAtegory is present on Store Front --> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!--Update Category--> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTreeUnderRoot(SimpleRootSubCategory.name)}}" stepKey="clickOnSubcategoryIsUndeRootCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCateforyToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!--Verify the Category is not present in Store Front--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> + <waitForPageLoad stepKey="waitForPageToLoaded2"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="dontSeeCatergoryInStoreFront"/> + + <!--Verify the Updated Category is present in Store Front--> + <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheUpdatedCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForPageToLoaded3"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml new file mode 100644 index 0000000000000..e7c4a8a093e19 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -0,0 +1,84 @@ +<?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="AdminUpdateCategoryUrlKeyWithStoreViewTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, URL key with custom store view"/> + <description value="Login as admin and update category URL Key with store view"/> + <testCaseId value="MC-6062"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Store Page --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + + <!--Create Custom Store --> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + + <!--Create Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Verify Category in Store View--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForSystemStorePage1"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Update URL Key--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded2"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory1"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> + <clearField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="clearUrlKeyField"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="newurlkey" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Open Category Store Front Page--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> + <waitForPageLoad stepKey="waitForSystemStorePage3"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory2"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + + <!--Verify Updated URLKey is present--> + <seeInCurrentUrl stepKey="verifyUpdatedUrlKey" url="newurlkey.html"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml new file mode 100644 index 0000000000000..3fea9c0eed7ca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml @@ -0,0 +1,83 @@ +<?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="AdminUpdateCategoryWithInactiveIncludeInMenuTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, name description urlkey metatitle exclude from menu"/> + <description value="Login as admin and update category name, description, urlKey, metatitle and exclude from menu"/> + <testCaseId value="MC-6058"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!--Update Category name,description, urlKey, meta title and disable Include in Menu--> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillCategoryName"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> + <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedUrlKey"/> + <fillField selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillUpdatedMetaTitle"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + + <!--Open UrlRewrite Page--> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!--Verify Updated Category UrlKey--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedCategoryUrlKey"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <see stepKey="seeCategoryUrlKey" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{SimpleRootSubCategory.url_key}}.html" /> + <!--Verify Updated Category UrlKey directs to category Store Front--> + <amOnPage url="{{SimpleRootSubCategory.url_key}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleRootSubCategory.name)}}" stepKey="seeUpdatedCategoryInStoreFrontPage"/> + + <!--Verify Updated fields in Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoaded1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCreatedCategory1"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryName"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent1"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent1"/> + <scrollTo selector="{{AdminCategoryContentSection.description}}" stepKey="scrollToDescription1"/> + <seeInField stepKey="seeUpdatedDiscription" selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <seeInField stepKey="seeUpdatedUrlKey" selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}"/> + <seeInField stepKey="seeUpdatedMetaTitleInput" selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml new file mode 100644 index 0000000000000..1cb01ac11cb8f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -0,0 +1,84 @@ +<?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="AdminUpdateCategoryWithProductsTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, sort products by default sorting"/> + <description value="Login as admin, update category and sort products"/> + <testCaseId value="MC-6059"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> + <createData entity="_defaultCategory" stepKey="createCategory"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + + <!--Update Product Display Setting--> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <scrollToTopOfPage stepKey="scfrollToTop"/> + <click selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="enableTheAvailableProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.productList}}" parameterArray="['Product Name', 'Price']" stepKey="selectPrice"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" x="0" y="-80" stepKey="scrollToDefaultProductList"/> + <click selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="enableTheDefaultProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.defaultProductList}}" userInput="name" stepKey="selectProductName"/> + + <!--Add Products in Category--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <scrollToTopOfPage stepKey="scrollOnTopOfPage"/> + <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct1"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitFroPageToLoad1"/> + <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProduct1FromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> + + <!--Verify Category Title--> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + + <!--Verify Category in store front page--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Verify Product in Category--> + <seeElement stepKey="seeProductsInCategory" selector="{{StorefrontCategoryMainSection.productLink}}"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + + <!--Verify product name and price on Store Front--> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{defaultSimpleProduct.price}}" stepKey="assertProductPrice"/> + </test> +</tests> + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml new file mode 100644 index 0000000000000..8872ea98eb504 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -0,0 +1,103 @@ +<?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="AdminUpdateFlatCategoryAndAddProductsTest"> + <annotations> + <stories value="Update category"/> + <title value="Flat Catalog - Assign Simple Product to Category"/> + <description value="Login as admin, update flat category by adding a simple product"/> + <testCaseId value="MC-11012"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!-- Create Simple Product --> + <createData entity="SimpleSubCategory" stepKey="category"/> + <!-- Create category --> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <magentoCLI command="indexer:reindex" 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" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select Created Category--> + <magentoCLI command="indexer:reindex" stepKey="reindexBeforeFlow"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForTheCategoryPageToLoaded"/> + <!--Add Products in Category--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <scrollToTopOfPage stepKey="scrollOnTopOfPage"/> + <conditionalClick selector="{{CatalogProductsSection.resetFilter}}" dependentSelector="{{CatalogProductsSection.resetFilter}}" visible="true" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForProductsToLoad"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$createSimpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitFroPageToLoad1"/> + <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Open Index Management Page and verify flat categoryIndex status--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <see stepKey="seeCategoryIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Product In Store Front--> + <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <!--Verify product and category is visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectFirstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + <!--Verify product and category is visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="seeProductName"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml new file mode 100644 index 0000000000000..5527303370623 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -0,0 +1,91 @@ +<?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="AdminUpdateFlatCategoryAndIncludeInMenuTest"> + <annotations> + <stories value="Update category"/> + <title value="Flat Catalog - Update Category, Include in Navigation Menu"/> + <description value="Login as admin and update flat category by enabling Include in Menu"/> + <testCaseId value="MC-11011"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="CatNotIncludeInMenu" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--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" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Verify Category is not listed in navigation menu--> + <amOnPage url="/{{CatNotIncludeInMenu.name_lwr}}.html" stepKey="openCategoryPage"/> + <waitForPageLoad time="60" stepKey="waitForPageToBeLoaded"/> + <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName(CatNotIncludeInMenu.name)}}" stepKey="dontSeeCategoryOnNavigation"/> + <!-- Select created category and enable Include In Menu option--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotIncludeInMenu.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <!--Verify category is visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnNavigation"/> + <!--Verify category is visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondstoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnNavigation1"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml new file mode 100644 index 0000000000000..fcbc0cb205268 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml @@ -0,0 +1,102 @@ +<?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="AdminUpdateFlatCategoryNameAndDescriptionTest"> + <annotations> + <stories value="Update category"/> + <title value="Flat Catalog - Update Category Name and Description"/> + <description value="Login as admin and update flat category name and description"/> + <testCaseId value="MC-11010"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--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" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select Created Category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Update Category Name and Description --> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> + <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="READY"/> + <!--Verify Category In Store Front--> + <amOnPage url="{{SimpleSubCategory.name}}.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <!--Verify category is visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectFirstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <!--Verify category is visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> + <!-- Verify Updated Category Name and description on Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectUpdatedCategory"/> + <waitForPageLoad stepKey="waitForUpdatedCategoryPageToLoad"/> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedSubCategoryName"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent1"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent1"/> + <seeInField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="seeUpdatedDescription"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml new file mode 100644 index 0000000000000..18e4ff9ee2c99 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -0,0 +1,87 @@ +<?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="AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product Name to Verify Data Overriding on Store View Level"/> + <description value="Test log in to Update Simple Product and Update Simple Product Name to Verify Data Overriding on Store View Level"/> + <testCaseId value="MC-10821"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{defaultSimpleProduct.sku}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Assign simple product to created store view --> + <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> + <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewOption(customStoreFR.name)}}" stepKey="selectCategoryStoreViewOption"/> + <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="clickAcceptButton"/> + <waitForPageLoad stepKey="waitForThePageToLoad"/> + <uncheckOption selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckProductStatus"/> + + <!-- Update default simple product with name --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDataOverriding.name}}" stepKey="fillSimpleProductName"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!--Verify customer see default simple product name on magento storefront page --> + <amOnPage url="{{StorefrontProductPage.url($$initialSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="$$initialSimpleProduct.name$$" stepKey="seeDefaultProductName"/> + + <!--Verify customer see simple product with updated name on magento storefront page under store view section --> + <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> + <waitForPageLoad stepKey="waitForStoreSwitcherLoad"/> + <click selector="{{StorefrontHeaderSection.storeView(customStoreFR.name)}}" stepKey="clickStoreViewOption"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearch"/> + <waitForPageLoad stepKey="waitForSearchTextBoxLoad"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextButton"/> + <waitForPageLoad stepKey="waitForTextSearchLoad"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductDataOverriding.name}}" stepKey="seeUpdatedSimpleProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml new file mode 100644 index 0000000000000..d5fc981b5b2e6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -0,0 +1,86 @@ +<?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="AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product Price to Verify Data Overriding on Store View Level"/> + <description value="Test log in to Update Simple Product and Update Simple Product Price to Verify Data Overriding on Store View Level"/> + <testCaseId value="MC-10823"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{defaultSimpleProduct.sku}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Assign simple product to created store view --> + <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> + <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewOption(customStoreFR.name)}}" stepKey="selectCategoryStoreViewOption"/> + <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="clickAcceptButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!-- Update default simple product with price --> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="fillSimpleProductPrice"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Verify customer see simple product with updated price on magento storefront page --> + <amOnPage url="{{StorefrontProductPage.url($$initialSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.regularPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="seeUpdatedProductPriceOnStorefrontPage"/> + + <!-- Verify customer see simple product with updated price on magento storefront page under store view section --> + <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> + <waitForPageLoad stepKey="waitForStoreSwitcherLoad"/> + <click selector="{{StorefrontHeaderSection.storeView(customStoreFR.name)}}" stepKey="clickStoreViewOption"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="$$initialSimpleProduct.sku$$" stepKey="fillDefaultSimpleProductSkuInSearch"/> + <waitForPageLoad stepKey="waitForSearchTextBoxLoad"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextButton"/> + <waitForPageLoad stepKey="waitForTextSearchLoad"/> + <see selector="{{StorefrontQuickSearchResultsSection.regularPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="seeUpdatedProductPriceOnStorefrontPageUnderStoreViewSection"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml new file mode 100644 index 0000000000000..2c3aa5db75171 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -0,0 +1,143 @@ +<?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="AdminUpdateSimpleProductTieredPriceTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product Tiered Price"/> + <description value="Test log in to Update Simple Product and Update Simple Product Tiered Price"/> + <testCaseId value="MC-10824"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductTierPrice300InStock.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with tier price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="fillSimpleProductPrice"/> + + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductTierPrice300InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductTierPrice300InStock.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductTierPrice300InStock.weight}}" stepKey="fillSimpleProductWeight"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductTierPrice300InStock.weightSelect}}" stepKey="selectProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="seeSimpleProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceHighCostSimpleProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceHighCostSimpleProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductTierPrice300InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> + <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"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductTierPrice300InStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="seeProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductTierPrice300InStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductTierPrice300InStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductTierPrice300InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml new file mode 100644 index 0000000000000..6e8f1ba6f12a6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -0,0 +1,93 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock), Disabled Product"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock), Disabled Product"/> + <testCaseId value="MC-10816"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductDisabled.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductDisabled.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDisabled.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductDisabled.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductDisabled.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductDisabled.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductDisabled.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProductLabelToDisableProduct"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductDisabled.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDisabled.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductDisabled.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDisabled.price}}" stepKey="seeSimpleProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductDisabled.quantity}}" stepKey="seeSimpleProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductDisabled.status}}" stepKey="seeSimpleProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductDisabled.weight}}" stepKey="seeSimpleProductWeight"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSectionHeader"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductDisabled.urlKey}}" stepKey="seeSimpleProductUrlKey"/> + + <!--Verify customer don't see updated simple product link on magento storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductDisabled.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductDisabled.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductDisabled.name}}" stepKey="dontSeeProductNameOnStorefrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml new file mode 100644 index 0000000000000..a042c4d60ae4f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml @@ -0,0 +1,141 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest"> + <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="LoginAsAdmin" 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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI stepKey="unsetFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0"/> + </after> + + <!-- Search default simple product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="fillSimpleProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="fillSimpleProductQuantity"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPage"/> + <conditionalClick selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" visible="true" stepKey="checkUseConfigSettingsCheckBox"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="selectManageStock"/> + <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductEnabledFlat.weight}}" stepKey="fillSimpleProductWeight"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductEnabledFlat.weightSelect}}" stepKey="selectProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductEnabledFlat.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductEnabledFlat.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="seeSimpleProductQuantity"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickTheAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPageLoad"/> + <see selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="seeManageStock"/> + <click selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="clickDoneButtonOnAdvancedInventory"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="seeSimpleProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductEnabledFlat.weight}}" stepKey="seeSimpleProductWeight"/> + <seeInField selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductEnabledFlat.weightSelect}}" stepKey="seeSimpleProductWeightSelect"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories" /> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductEnabledFlat.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductEnabledFlat.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductEnabledFlat.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductEnabledFlat.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductEnabledFlat.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductEnabledFlat.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnMagentoStorefrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml new file mode 100644 index 0000000000000..d08ef9c93999c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.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="AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Not Visible Individually"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Not Visible Individually"/> + <testCaseId value="MC-10803"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductNotVisibleIndividually.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductNotVisibleIndividually.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductNotVisibleIndividually.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductNotVisibleIndividually.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductNotVisibleIndividually.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductNotVisibleIndividually.urlKey}}" stepKey="fillSimpleProductUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductNotVisibleIndividually.price}}" stepKey="seeSimpleProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductNotVisibleIndividually.quantity}}" stepKey="seeSimpleProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductNotVisibleIndividually.status}}" stepKey="seeSimpleProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductNotVisibleIndividually.weight}}" stepKey="seeSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories" /> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="seeSimpleProductVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductNotVisibleIndividually.urlKey}}" stepKey="seeSimpleProductUrlKey"/> + + <!--Verify customer don't see updated simple product link on magento storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductNotVisibleIndividually.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductNotVisibleIndividually.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="dontSeeSimpleProductNameOnMagentoStorefrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml new file mode 100644 index 0000000000000..3433a09117322 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -0,0 +1,68 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Unassign from Category"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Unassign from Category"/> + <testCaseId value="MC-10817"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="_defaultProduct" stepKey="initialSimpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search default simple product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product by unselecting categories --> + <scrollTo selector="{{AdminProductFormSection.productStockStatus}}" stepKey="scroll"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <click selector="{{AdminProductFormSection.unselectCategories($$initialCategoryEntity.name$$)}}" stepKey="unselectCategories"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategory"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!--Search default simple product in the grid page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="OpenCategoryCatalogPage"/> + <waitForPageLoad stepKey="waitForCategoryCatalogPage"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$initialCategoryEntity.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="clickAdminCategoryProductSection"/> + <waitForPageLoad stepKey="waitForSectionHeaderToLoad"/> + <dontSee selector="{{AdminCategoryProductsGridSection.rowProductName($$initialSimpleProduct.name$$)}}" stepKey="dontSeeProductNameOnCategoryCatalogPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml new file mode 100644 index 0000000000000..a695982921cfd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -0,0 +1,126 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Visible in Catalog and Search"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Visible in Catalog and Search"/> + <testCaseId value="MC-10802"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPrice245InStock.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice245InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice245InStock.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice245InStock.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="fillSimpleProductUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="seeSimpleProductPrice"/> + <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" /> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice245InStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPrice245InStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPrice245InStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice245InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnStorefrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml new file mode 100644 index 0000000000000..ba52c6d2bc261 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -0,0 +1,126 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Visible in Catalog Only"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Visible in Catalog Only"/> + <testCaseId value="MC-10804"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPrice32501InStock.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <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"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32501InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice32501InStock.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32501InStock.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="seeSimpleProductPrice"/> + <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" /> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32501InStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="seeProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPrice32501InStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPrice32501InStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32501InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="dontSeeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml new file mode 100644 index 0000000000000..cb5c24839e387 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -0,0 +1,126 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Visible in Search Only"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Visible in Search Only"/> + <testCaseId value="MC-10805"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPrice325InStock.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice325InStock.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice325InStock.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice325InStock.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice325InStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="seeSimpleProductPrice"/> + <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" /> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice325InStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="dontSeeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice325InStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="seeProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPrice325InStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPrice325InStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice325InStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml new file mode 100644 index 0000000000000..318ab6555235e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -0,0 +1,159 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) with Custom Options"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) with Custom Options"/> + <testCaseId value="MC-10819"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPriceCustomOptions.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> + + <!-- Create simple product with customizable option --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> + <waitForPageLoad stepKey="waitForDataToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{simpleProductCustomizableOption.title}}" stepKey="fillOptionTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDown"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', simpleProductCustomizableOption.type)}}" stepKey="selectOptionFieldFromDropDown"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBox"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddValueButton"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_title}}" stepKey="fillOptionTitleForCustomizableOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price}}" stepKey="fillOptionPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="fillOptionSku"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!--Verify customer see success message--> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!--Search updated simple product(from above step) in the grid page--> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPrice"/> + <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" /> + <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"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOptionToSeeValues"/> + + <!-- Verify simple product with customizable options --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForCustomizableOption"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{simpleProductCustomizableOption.title}}" stepKey="seeOptionTitleForCustomizableOption"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownForCustomizableOption"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', simpleProductCustomizableOption.type)}}" stepKey="selectOptionFieldFromDropDownForCustomizableOption"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForTheThirdDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_title}}" stepKey="seeOptionTitle"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price}}" stepKey="seeOptionPrice"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionValuePriceType"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="seeOptionSku"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPriceCustomOptions.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPriceCustomOptions.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPriceCustomOptions.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer see customizable options are Required --> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(simpleProductCustomizableOption.title)}}" stepKey="verifyFirstCustomOptionIsRequired"/> + + <!--Verify customer see customizable option titles and prices --> + <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(simpleProductCustomizableOption.title)}}" stepKey="simpleOptionId"/> + <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$simpleOptionId})}}" stepKey="grabFourthOptions"/> + <assertEquals stepKey="assertFourthSelectOptions"> + <actualResult type="variable">grabFourthOptions</actualResult> + <expectedResult type="array">['-- Please Select --', {{simpleProductCustomizableOption.option_0_title}} +$98.00]</expectedResult> + </assertEquals> + + <!-- Verify added Product in cart --> + <selectOption selector="{{StorefrontProductPageSection.customOptionDropDown}}" userInput="{{simpleProductCustomizableOption.option_0_title}} +$98.00" stepKey="selectCustomOption"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeYouAddedSimpleprod4ToYourShoppingCartSuccessSaveMessage"/> + <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeProductNameInMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{simpleProductRegularPriceCustomOptions.storefront_new_cartprice}}" stepKey="seeProductPriceInMiniCart"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml new file mode 100644 index 0000000000000..54ed753b80a1c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -0,0 +1,124 @@ +<?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="AdminUpdateSimpleProductWithRegularPriceOutOfStockTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (Out of Stock)"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (Out of Stock)"/> + <testCaseId value="MC-10806"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <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="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPrice32503OutOfStock.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPrice"/> + <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" /> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="dontSeeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32503OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPrice32503OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPrice32503OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32503OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="dontSeeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml new file mode 100644 index 0000000000000..4dea6663e61bf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml @@ -0,0 +1,88 @@ +<?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="AdminUpdateTopCategoryUrlWithNoRedirectTest"> + <annotations> + <stories value="Update category"/> + <title value="Update top category url and do not create redirect"/> + <description value="Login as admin and update top category url and do not create redirect"/> + <testCaseId value="MC-6056"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create three level nested category --> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> + <requiredEntity createDataKey="createTwoLevelNestedCategories"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> + <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + </after> + + <!-- Open Category page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!-- Open 3rd Level category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Update category UrlKey and uncheck permanent redirect for old URL --> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updatedurl" stepKey="updateUrlKey"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckPermanentRedirectCheckBox"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Get Category Id --> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open Url Rewrite Page --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!-- Verify third level category's Redirect Path, Target Path and Redirect Type after the URL Update --> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad0"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="updatedurl" stepKey="fillUpdatedUrlInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <see stepKey="seeTheRedirectType" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> + <see stepKey="seeTheTargetPath" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updatedurl.html" stepKey="seeTheRedirectPath"/> + + <!-- Verify third level category's old URL path doesn't show redirect path--> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{Three_nested_categories.name_lwr}}" stepKey="fillOldUrlInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + <see stepKey="seeEmptyRecodsMessage" selector="{{AdminUrlRewriteIndexSection.emptyRecords}}" userInput="We couldn't find any records."/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml new file mode 100644 index 0000000000000..ee1ed5f97edfa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml @@ -0,0 +1,88 @@ +<?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="AdminUpdateTopCategoryUrlWithRedirectTest"> + <annotations> + <stories value="Update category"/> + <title value="Update top category url and create redirect"/> + <description value="Login as admin and update top category url and create redirect"/> + <testCaseId value="MC-6057"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Create three level nested category --> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> + <requiredEntity createDataKey="createTwoLevelNestedCategories"/> + </createData> + </before> + <after> + <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> + <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!-- Open 3rd Level category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Update category UrlKey and check permanent redirect for old URL --> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updateredirecturl" stepKey="updateUrlKey"/> + <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkPermanentRedirectCheckBox"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Get Category ID --> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open Url Rewrite Page --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!-- Verify third level category's Redirect Path, Target Path and Redirect Type after the URL update --> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="updateredirecturl" stepKey="fillUpdatedURLInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" stepKey="seeTheRedirectType"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}" stepKey="seeTheTargetPath"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updateredirecturl.html" stepKey="seeTheRedirectPath"/> + + <!-- Verify third level category's Redirect path, Target Path and Redirect type for old URL --> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$$createThreeLevelNestedCategories.name$$" stepKey="fillOldUrlInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad5"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/$$createThreeLevelNestedCategories.name$$.html" stepKey="seeTheRedirectPathForOldUrl"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updateredirecturl.html" stepKey="seeTheTargetPathForOldUrl"/> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" stepKey="seeTheRedirectTypeForOldUrl"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml new file mode 100644 index 0000000000000..9bdc93e61e499 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -0,0 +1,155 @@ +<?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="AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (In Stock) Visible in Category Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (In Stock) Visible in Category Only"/> + <testCaseId value="MC-6495"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPrice.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we see created virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPrice.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="seeUrlKey"/> + + <!-- Verify customer don't see updated virtual product link on storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillVirtualProductSkuOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="dontSeeVirtualProductName"/> + + <!-- Verify customer see updated virtual product in category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductLinkOnCategoryPage"/> + <grabTextFrom selector="{{StorefrontCategoryMainSection.asLowAs}}" stepKey="tierPriceTextOnCategoryPage"/> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnCategoryPage</actualResult> + </assertEquals> + + <!-- Verify customer see updated virtual product and tier price on product page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice.urlKey)}}" stepKey="goToStorefrontProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml new file mode 100644 index 0000000000000..34d85e7b46850 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -0,0 +1,249 @@ +<?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="AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (In Stock) with Custom Options Visible in Search Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (In Stock) with Custom Options Visible in Search Only"/> + <testCaseId value="MC-6641"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPriceInStock.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPriceInStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPriceInStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> + <!-- Create virtual product with customizable options dataSet1 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> + <waitForPageLoad stepKey="waitForFirstOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="fillOptionTitleForFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="fillOptionPriceForFirstDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="fillOptionSkuForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForFirstDataSet"/> + <!--Create virtual product with customizable options dataSet2 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSet"/> + <waitForPageLoad stepKey="waitForSecondDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="fillOptionTitleForSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="fillOptionPriceForSecondDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="fillOptionSkuForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForSecondDataSet"/> + <!-- Create virtual product with customizable options dataSet3 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForThirdSetOfData"/> + <waitForPageLoad stepKey="waitForThirdSetOfDataToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="fillOptionTitleForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForThirdDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="fillOptionTitleForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="fillOptionPriceForThirdDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="fillOptionSkuForThirdDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="fillOptionTitleForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="fillOptionPriceForThirdDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="fillOptionSkuForThirdDataSetSecondRow"/> + <!-- Create virtual product with customizable options dataSet4 --> + <scrollToTopOfPage stepKey="scrollToAddOptionButton"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForFourthDataSet"/> + <waitForPageLoad stepKey="waitForFourthDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForFourthSetOfData"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForFourthDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="fillOptionTitleForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="fillOptionPriceForFourthDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="fillOptionSkuForFourthDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="fillOptionTitleForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="fillOptionPriceForFourthDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we customer see updated virtual product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPriceInStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPriceInStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="seeUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOptionToSeeValues"/> + <!-- Create virtual product with customizable options dataSet1 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton1"/> + <waitForPageLoad stepKey="waitForFirstOptionToLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="seeOptionTitleForFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet1"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet1"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet1"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="seeOptionPriceForFirstDataSet"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet1"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="seeOptionSkuForFirstDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="seeOptionMaxCharactersForFirstDataSet"/> + <!--Create virtual product with customizable options dataSet2 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSetToSeeFields"/> + <waitForPageLoad stepKey="waitForTheSecondDataSetToLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="seeOptionTitleForSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet2"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet2"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForTheSecondDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="seeOptionPriceForSecondDataSet"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheSecondDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="seeOptionSkuForSecondDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="seeOptionMaxCharactersForSecondDataSet"/> + <!-- Create virtual product with customizable options dataSet3 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForTheThirdSetOfData"/> + <waitForPageLoad stepKey="waitForTheThirdSetOfDataToLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="seeOptionTitleForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForTheThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForTheThirdDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForTheThirdDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="seeOptionTitleForThirdDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="seeOptionPriceForThirdDataSetFirstRow"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheThirdDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="seeOptionSkuForThirdDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="seeOptionTitleForThirdDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="seeOptionPriceForThirdDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForTheThirdDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="seeOptionSkuForThirdDataSetSecondRow"/> + <!-- Create virtual product with customizable options dataSet4 --> + <scrollToTopOfPage stepKey="scrollToTheAddOptionButton"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForTheFourthDataSet"/> + <waitForPageLoad stepKey="waitForTheFourthDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForTheFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForTheFourthSetOfData"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForTheFourthDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForTheFourthDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="seeOptionTitleForFourthDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="seeOptionPriceForFourthDataSetFirstRow"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheFourthDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="seeOptionSkuForFourthDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="seeOptionTitleForFourthDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="seeOptionPriceForFourthDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForTheFourthDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="seeOptionSkuForFourthDataSetSecondRow"/> + + <!--Verify customer don't see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="dontSeeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see updated virtual product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPriceInStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPriceInStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPriceInStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!--Verify we customer see customizable options are Required --> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> + <!--Verify customer see customizable option titles and prices --> + <grabMultiple selector="{{StorefrontProductInfoMainSection.allCustomOptionLabels}}" stepKey="allCustomOptionLabels" /> + <assertEquals stepKey="verifyLabels"> + <actualResult type="variable">allCustomOptionLabels</actualResult> + <expectedResult type="array">[{{virtualProductCustomizableOption1.title}} + ${{virtualProductCustomizableOption1.option_0_price}}, {{virtualProductCustomizableOption2.title}} + ${{virtualProductCustomizableOption2.option_0_price}}, {{virtualProductCustomizableOption3.title}}, {{virtualProductCustomizableOption4.title}}]</expectedResult> + </assertEquals> + <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(virtualProductCustomizableOption4.title)}}" stepKey="fourthOptionId" /> + <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$fourthOptionId})}}" stepKey="grabFourthOptions" /> + <assertEquals stepKey="assertFourthSelectOptions"> + <actualResult type="variable">grabFourthOptions</actualResult> + <expectedResult type="array">['-- Please Select --', {{virtualProductCustomizableOption4.option_0_title}} +$12.01, {{virtualProductCustomizableOption4.option_1_title}} +$20.02]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 0000000000000..a2a4f65860254 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,100 @@ +<?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="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Category and Search"/> + <testCaseId value="MC-7433"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with regular price(out of stock) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="seeProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product with regular price(out of stock) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice5OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml new file mode 100644 index 0000000000000..e64022b311614 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml @@ -0,0 +1,127 @@ +<?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="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Category Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Category Only"/> + <testCaseId value="MC-6503"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickclearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we see updated virtual product with regular price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated virtual product link(from above step) on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="dontseeVirtualProductNameOnCategoryPage"/> + + <!--Verify customer see updated virtual product (from above step) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice5OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link(from above step) on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="dontSeeVirtualProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml new file mode 100644 index 0000000000000..aa3184994daff --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml @@ -0,0 +1,112 @@ +<?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="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Search Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Search Only"/> + <testCaseId value="MC-6498"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with regular price in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.status}}" stepKey="seeProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product on storefront page by url key --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice99OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice99OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice99OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice99OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="dontSeeVirtualProductLinkOnStorefrontPage"/> + + <!--Verify customer don't see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$initialCategoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="dontSeeVirtualProductLinkOnCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 0000000000000..9b6a56d6f81d8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,143 @@ +<?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="AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Special Price (In Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Special Price (In Stock) Visible in Category and Search"/> + <testCaseId value="MC-6496"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with special price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPrice.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPrice.special_price}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPrice.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductSpecialPrice.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductSpecialPrice.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + <!-- Verify customer see updated virtual product with special price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPrice.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPrice.special_price}}" stepKey="seeSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPrice.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductSpecialPrice.quantity}}" stepKey="seeProductQuantity"/> + <see selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductSpecialPrice.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see updated virtual product on the magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductName"/> + + <!--Verify customer see updated virtual product with special price(from above step) on product storefront page by url key --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeVirtualProductSku"/> + <!-- Verify customer see virtual product special price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPrice.special_price}}</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see virtual product old price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPrice.price}}</expectedResult> + <actualResult type="variable">oldPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see virtual product in stock status on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductSpecialPrice.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 0000000000000..920a0a494bae5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,135 @@ +<?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="UpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Special Price (Out of Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Special Price (Out of Stock) Visible in Category and Search"/> + <testCaseId value="MC-6505"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with special price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.special_price}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product with special price(out of stock) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + <!-- Verify customer see updated virtual product with special price(out of stock) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.special_price}}" stepKey="seeSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <see selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product with special price(out of stock) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPriceOutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductSpecialPriceOutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPriceOutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!--Verify customer see virtual product with special price on the storefront page--> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPriceOutOfStock.special_price}}</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPriceOutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductNameOnStorefrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 0000000000000..d4ec5e410d9ff --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,155 @@ +<?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="UpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Tier Price (In Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (In Stock) Visible in Category and Search"/> + <testCaseId value="MC-6504"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with tier price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductTierPriceInStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductTierPriceInStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductTierPriceInStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductTierPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductTierPriceInStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductTierPriceInStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductTierPriceInStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductLinkOnCategoryPage"/> + + <!--Verify customer see updated virtual product with tier price on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductTierPriceInStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductTierPriceInStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductTierPriceInStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 38%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + + <!--Verify customer see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductTierPriceInStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductName"/> + <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml new file mode 100644 index 0000000000000..717d710b4a288 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -0,0 +1,151 @@ +<?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="AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Tier Price (In Stock) Visible in Category Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (In Stock) Visible in Category Only"/> + <testCaseId value="MC-7508"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with tier price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductWithTierPriceInStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductWithTierPriceInStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductWithTierPriceInStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductWithTierPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductWithTierPriceInStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductWithTierPriceInStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!--Verify customer see updated virtual product with tier price(from above step) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductWithTierPriceInStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductWithTierPriceInStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductWithTierPriceInStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductWithTierPriceInStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="dontSeeVirtualProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 0000000000000..703a4e24cdca9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,151 @@ +<?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="AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Tier Price (Out of Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (Out of Stock) Visible in Category and Search"/> + <testCaseId value="MC-6499"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with tier price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualTierPriceOutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualTierPriceOutOfStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualTierPriceOutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we customer see updated virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualTierPriceOutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualTierPriceOutOfStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualTierPriceOutOfStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductNameOnCategoryPage"/> + + <!--Verify customer see updated virtual product with tier price(from above step) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualTierPriceOutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualTierPriceOutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualTierPriceOutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 51%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualTierPriceOutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml new file mode 100644 index 0000000000000..a81c26b6e6eaf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml @@ -0,0 +1,31 @@ +<?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="AdminVerifyProductOrder"> + <annotations> + <features value="Catalog"/> + <stories value="Verify Product Order"/> + <title value="Admin should see product types in specified order"/> + <description value="Product Type Order should be Simple -> Configurable -> Grouped -> Virtual -> Bundle -> Downloadable -> EE"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14207"/> + <group value="mtf_migrated"/> + <group value="product"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="VerifyProductTypeOrder" stepKey="verifyProductTypeOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml index 0eb8f5668751a..84c3f81ef6dbf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchVirtualProductByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 7da668df59022..cee40241185b4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="CheckTierPricingOfProductsTest"> <annotations> <features value="Shopping Cart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml new file mode 100644 index 0000000000000..d895993217e32 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml @@ -0,0 +1,425 @@ +<?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="CreateProductAttributeEntityTextFieldTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a TextField product attribute"/> + <description value="Admin should be able to create a TextField product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10894"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttributeWithTextField" stepKey="createAttribute"> + <argument name="attribute" value="textProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute.--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Perform appropriate assertions against textProductAttribute entity--> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{textProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{textProductAttribute.frontend_input}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{textProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{textProductAttribute.attribute_code}}"/> + <seeInField stepKey="assertDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueText}}" userInput="{{textProductAttribute.default_value}}"/> + + <!--Go to New Product page, add Attribute and check values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(textProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeInField stepKey="checkDefaultValue" selector="{{AdminProductAttributesSection.attributeTextInputByCode(textProductAttribute.attribute_code)}}" userInput="{{textProductAttribute.default_value}}"/> + <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(textProductAttribute.attribute_code)}}" userInput="{{textProductAttribute.attribute_code}}"/> + </test> + + <test name="CreateProductAttributeEntityDateTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Date product attribute"/> + <description value="Admin should be able to create a Date product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10895"/> + <group value="Catalog"/> + <skip> + <issueId value="MC-13817"/> + </skip> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Generate date for use as default value, needs to be MM/d/YYYY --> + <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttributeWithDateField" stepKey="createAttribute"> + <argument name="attribute" value="dateProductAttribute"/> + <argument name="date" value="{$generateDefaultDate}"/> + </actionGroup> + + <!--Navigate to Product Attribute.--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Perform appropriate assertions against textProductAttribute entity--> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dateProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dateProductAttribute.frontend_input}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dateProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dateProductAttribute.attribute_code}}"/> + <seeInField stepKey="assertDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueDate}}" userInput="{$generateDefaultDate}"/> + + <!--Go to New Product page, add Attribute and check values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(dateProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeInField stepKey="checkDefaultValue" selector="{{AdminProductAttributesSection.attributeTextInputByCode(dateProductAttribute.attribute_code)}}" userInput="{$generateDefaultDate}"/> + <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(dateProductAttribute.attribute_code)}}" userInput="{{dateProductAttribute.attribute_code}}"/> + </test> + + <test name="CreateProductAttributeEntityPriceTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Price product attribute"/> + <description value="Admin should be able to create a Price product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10897"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{priceProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute with Price--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="priceProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute.--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{priceProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Perform appropriate assertions against priceProductAttribute entity--> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{priceProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{priceProductAttribute.frontend_input}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{priceProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{priceProductAttribute.attribute_code}}"/> + + <!--Go to New Product page, add Attribute and check values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{priceProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(priceProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(priceProductAttribute.attribute_code)}}" userInput="{{priceProductAttribute.attribute_code}}"/> + </test> + + <test name="CreateProductAttributeEntityDropdownTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Dropdown product attribute"/> + <description value="Admin should be able to create a Dropdown product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10896"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="dropdownProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Options and Save - 1--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption1"> + <argument name="adminName" value="{{dropdownProductAttribute.option1_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttribute.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption2"> + <argument name="adminName" value="{{dropdownProductAttribute.option2_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttribute.option2_frontend}}"/> + <argument name="row" value="2"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption3"> + <argument name="adminName" value="{{dropdownProductAttribute.option3_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttribute.option3_frontend}}"/> + <argument name="row" value="3"/> + </actionGroup> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <!--Perform appropriate assertions against dropdownProductAttribute entity--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> + <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dropdownProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dropdownProductAttribute.frontend_input_admin}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dropdownProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dropdownProductAttribute.attribute_code}}"/> + + <!--Assert options are in order and with correct attributes--> + <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{dropdownProductAttribute.option1_admin}}"/> + <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{dropdownProductAttribute.option1_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> + <seeInField stepKey="seeOption2Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('2')}}" userInput="{{dropdownProductAttribute.option2_admin}}"/> + <seeInField stepKey="seeOption2StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('2')}}" userInput="{{dropdownProductAttribute.option2_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption2Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('2')}}"/> + <seeInField stepKey="seeOption3Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('3')}}" userInput="{{dropdownProductAttribute.option3_admin}}"/> + <seeInField stepKey="seeOption3StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('3')}}" userInput="{{dropdownProductAttribute.option3_frontend}}"/> + <seeCheckboxIsChecked stepKey="seeOption3Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('3')}}"/> + + <!--Go to New Product page, add Attribute and check dropdown values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_admin}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option1_admin}}"/> + <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option2_admin}}"/> + <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_admin}}"/> + </test> + + <test name="CreateProductAttributeEntityDropdownWithSingleQuoteTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Dropdown product attribute containing a single quote"/> + <description value="Admin should be able to create a Dropdown product attribute containing a single quote"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10898"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="dropdownProductAttributeWithQuote"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Option and Save - 1--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption1"> + <argument name="adminName" value="{{dropdownProductAttributeWithQuote.option1_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <!--Perform appropriate assertions against dropdownProductAttribute entity--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> + <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dropdownProductAttributeWithQuote.frontend_input_admin}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dropdownProductAttributeWithQuote.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + + <!--Assert options are in order and with correct attributes--> + <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}"/> + <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> + <seeCheckboxIsChecked stepKey="seeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> + + <!--Go to New Product page, add Attribute and check dropdown values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" stepKey="waitforLabel"/> + <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}"/> + </test> + + <test name="CreateProductAttributeEntityMultiSelectTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a MultiSelect product attribute"/> + <description value="Admin should be able to create a MultiSelect product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10888"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="multiselectProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Options and Save - 1--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption1"> + <argument name="adminName" value="{{multiselectProductAttribute.option1_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption2"> + <argument name="adminName" value="{{multiselectProductAttribute.option2_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option2_frontend}}"/> + <argument name="row" value="2"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption3"> + <argument name="adminName" value="{{multiselectProductAttribute.option3_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option3_frontend}}"/> + <argument name="row" value="3"/> + </actionGroup> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <!--Perform appropriate assertions against multiselectProductAttribute entity--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{multiselectProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{multiselectProductAttribute.frontend_input_admin}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{multiselectProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{multiselectProductAttribute.attribute_code}}"/> + + <!--Assert options are in order and with correct attributes--> + <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{multiselectProductAttribute.option1_admin}}"/> + <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{multiselectProductAttribute.option1_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> + <seeInField stepKey="seeOption2Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('2')}}" userInput="{{multiselectProductAttribute.option2_admin}}"/> + <seeInField stepKey="seeOption2StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('2')}}" userInput="{{multiselectProductAttribute.option2_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption2Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('2')}}"/> + <seeInField stepKey="seeOption3Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('3')}}" userInput="{{multiselectProductAttribute.option3_admin}}"/> + <seeInField stepKey="seeOption3StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('3')}}" userInput="{{multiselectProductAttribute.option3_frontend}}"/> + <seeCheckboxIsChecked stepKey="seeOption3Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('3')}}"/> + + <!--Go to New Product page, add Attribute and check multiselect values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_admin}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option1_admin}}"/> + <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option2_admin}}"/> + <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_admin}}"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml new file mode 100644 index 0000000000000..e79e4cea408fb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.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="DeleteUsedInConfigurableProductAttributeTest"> + <annotations> + <stories value="Delete product attribute"/> + <title value="Admin should not be able to delete product attribute used in configurable product"/> + <description value="Admin should not be able to delete product attribute used in configurable product"/> + <testCaseId value="MC-14061"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="categoryHandle"/> + + <!-- Create base configurable product--> + <createData entity="BaseConfigurableProduct" stepKey="baseConfigProductHandle"> + <requiredEntity createDataKey="categoryHandle"/> + </createData> + + <!-- Create Dropdown Product Attribute --> + <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> + + <!--Create attribute options --> + <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <!--Add to attribute to Default attribute set--> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <!-- Get handle of the attribute options --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + + <!--Create configurable product with the options --> + <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> + <requiredEntity createDataKey="baseConfigProductHandle"/> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption1Handle"/> + <requiredEntity createDataKey="getAttributeOption2Handle"/> + </createData> + + <!-- Login As Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete the configurable product created in the before block --> + <deleteData createDataKey="baseConfigProductHandle" stepKey="deleteConfig"/> + + <!-- Delete the category created in the before block --> + <deleteData createDataKey="categoryHandle" stepKey="deleteCategory"/> + + <!-- Delete configurable product attribute created in the before block --> + <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!-- Go to Stores > Attributes > Products. Search and select the product attribute that was used to create the configurable product--> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> + </actionGroup> + + <!-- Click Delete Attribute button --> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> + </actionGroup> + + <!-- Should see error message: This attribute is used in configurable products. --> + <actionGroup ref="AssertAttributeDeletionErrorMessageActionGroup" stepKey="assertAttributeDeletionErrorMessage"/> + + <!-- Go back to the product attribute grid. Should see the product attribute in the grid --> + <actionGroup ref="SearchAttributeByCodeOnProductAttributeGridActionGroup" stepKey="searchAttributeByCodeOnProductAttributeGrid"> + <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> + </actionGroup> + + <!-- Go to the Catalog > Products page and search configurable product created in before block.--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnBackend"> + <argument name="product" value="$$baseConfigProductHandle$$"/> + </actionGroup> + + <!--Should see the product attributes as expected --> + <actionGroup ref="AssertProductAttributePresenceInCatalogProductGridActionGroup" stepKey="assertProductAttributePresenceInCatalogProductGrid"> + <argument name="productAttribute" value="$$productAttributeHandle$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml new file mode 100644 index 0000000000000..3dd55a9dfee92 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.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="ProductAvailableAfterEnablingSubCategoriesTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that parent categories are showing products after enabling subcategories after fully reindex"/> + <description value="Check that parent categories are showing products after enabling subcategories after fully reindex"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97370"/> + <useCaseId value="MAGETWO-96846"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory"> + <requiredEntity createDataKey="createCategory"/> + <field key="is_active">false</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="simpleSubCategory"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> + <waitForPageLoad stepKey="waitForCategoryStorefront"/> + <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoadAddProducts"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> + <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> + </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 9c9f09f807eaf..fb95fc3f57bca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -63,6 +63,14 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> <argument name="customStore" value="customStoreFR"/> </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearWebsitesGridFilters"/> + + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> </after> <!-- Open Product Grid, Filter product and open --> @@ -74,8 +82,9 @@ <argument name="product" value="_defaultProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> <!-- Update Product with Option Value DropDown 1--> <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> @@ -97,15 +106,12 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> <!-- Switcher to Store FR--> - <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> - - <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> + <argument name="storeView" value="customStoreFR.name"/> + </actionGroup> <!-- Open tab Customizable Options --> - <waitForPageLoad time="10" stepKey="waitForPageLoad4"/> <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> <!-- Update Option Customizable Options and Option Value 1--> @@ -125,11 +131,9 @@ <!-- Login Customer Storefront --> - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad6"/> - <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> <!-- Go to Product Page --> @@ -172,23 +176,29 @@ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> <!-- Place Order --> - <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder1"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> <!-- Open Order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForPageLoad stepKey="waitForPageLoadOrdersPage"/> - <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> + <actionGroup ref="filterOrderGridById" stepKey="openOrdersGrid"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> @@ -200,14 +210,12 @@ <!-- Switch to FR Store View Storefront --> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad11"/> - <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher1"/> - <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown1"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(customStoreFR.code)}}" stepKey="selectStoreView1"/> - <waitForPageLoad stepKey="waitForPageLoad12"/> - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad13"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> @@ -245,13 +253,22 @@ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad14"/> <!-- Place Order --> - <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder2"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> + + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder2"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> <!-- Open Product Grid, Filter product and open --> @@ -291,8 +308,7 @@ <!--Go to Product Page--> - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page2"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad20"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index d96399738c80a..a3bce2d4fe2f2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -17,35 +17,39 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-61717"/> <group value="Catalog"/> - <!-- skip due to MAGETWO-97424 --> - <group value="skip"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Create Simple Product with Custom Options--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">17</field> + </createData> + <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> + <!-- Logout customer before in case of it logged in from previous test --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> </before> <after> <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="logout" stepKey="logoutAdmin"/> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> </after> - <!--Create Simple Product with Custom Options--> + <!-- Login Customer Storefront --> - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="_defaultProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> - <field key="price">17</field> - </createData> - <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> - - <!-- Login Customer Storeront --> - - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomerOnStorefront"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> <!-- Checking the correctness of displayed prices for user parameters --> - <amOnPage url="{{StorefrontHomePage.url}}$createProduct.custom_attributes[url_key]$.html" stepKey="amOnProduct3Page"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionField.title, ProductOptionField.price)}}" stepKey="checkFieldProductOption"/> <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionArea.title, '1.7')}}" stepKey="checkAreaProductOption"/> @@ -82,7 +86,7 @@ <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> - <argument name="productName" value="$createProduct.name$"/> + <argument name="productName" value="$$createProduct.name$$"/> </actionGroup> <!-- Checking the correctness of displayed custom options for user parameters on checkout --> @@ -94,21 +98,21 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> - <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$createProduct.name$" stepKey="seeProductInCart"/> - - <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($createProduct.name$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" visible="false" stepKey="exposeProductOptions"/> - - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> - <seeElement selector="{{CheckoutPaymentSection.ProductOptionLinkActiveByProductItemName($createProduct.name$, productWithOptions.file)}}" stepKey="seeProductOptionFileInputLink1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="Jan 1, $year" stepKey="seeProductOptionDateAndTimeInput" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeProductOptionDataInput" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + + <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($$createProduct.name$$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" visible="false" stepKey="exposeProductOptions"/> + + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> + <seeElement selector="{{CheckoutPaymentSection.ProductOptionLinkActiveByProductItemName($$createProduct.name$$, productWithOptions.file)}}" stepKey="seeProductOptionFileInputLink1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="Jan 1, $year" stepKey="seeProductOptionDateAndTimeInput" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeProductOptionDataInput" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> @@ -126,13 +130,11 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> - <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> - <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> - <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <actionGroup ref="filterOrderGridById" stepKey="filterByOrderId"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageOpened"/> <!-- Checking the correctness of displayed custom options for user parameters on Order --> @@ -170,24 +172,18 @@ <!-- Go to Customer Order Page and Checking the correctness of displayed custom options for user parameters on Order --> - <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnProduct4Page"/> + <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnOrderPage"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($$createProduct.name$$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> <seeElement selector="{{StorefrontCustomerOrderSection.productCustomOptionsLink($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" stepKey="seeStorefontOrderProductOptionFileLink1"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDate.title, 'Jan 1, $year')}}" userInput="Jan 1, $year" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDateTime.title, '1/1/$shortYear, 1:00 AM')}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> - - <!-- Delete product and category --> - - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDate.title, 'Jan 1, $year')}}" userInput="Jan 1, $year" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDateTime.title, '1/1/$shortYear, 1:00 AM')}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml new file mode 100644 index 0000000000000..268e18d2b4efa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml @@ -0,0 +1,94 @@ +<?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="StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> + <description value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97508"/> + <useCaseId value="MAGETWO-96847"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!--Delete create data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Set timezone for default config--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig"/> + <waitForPageLoad stepKey="waitForConfigPage"/> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Central European Standard Time (Europe/Paris)" stepKey="setTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> + + <!--Set timezone for Main Website--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig1"/> + <waitForPageLoad stepKey="waitForConfigPage1"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection1"/> + <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault"/> + <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone1"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Greenwich Mean Time (Africa/Abidjan)" stepKey="setTimezone1"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig1"/> + + <!--Set special price to created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="setSpecialPriceToCreatedProduct"> + <argument name="price" value="15"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Login to storefront from customer and check price--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Go to the product page and check special price--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceValue}}" stepKey="grabSpecialPrice"/> + <assertEquals expected='$15.00' expectedType="string" actual="$grabSpecialPrice" stepKey="assertSpecialPrice"/> + + <!--Reset timezone--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset"/> + <waitForPageLoad stepKey="waitForConfigPageReset"/> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset"/> + + <!--Reset timezone--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset1"/> + <waitForPageLoad stepKey="waitForConfigPageReset1"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup1"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset1"/> + <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault1"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone1"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml index f283a040ced41..4d7c97b26457c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest"> <annotations> <features value="Catalog"/> @@ -84,4 +84,4 @@ <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickOnUpdateShoppingCartButton"/> <seeInField userInput="5.5" selector="{{CheckoutCartProductSection.ProductQuantityByName(('$$createPreReqSimpleProduct.name$$'))}}" stepKey="seeInField2"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php index 1dd866f1fe2ca..da35d845468d5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php @@ -96,7 +96,12 @@ public function testGetRssData() $this->urlBuilder->expects($this->once())->method('getUrl') ->with('catalog/product/edit', ['id' => 1, '_secure' => true, '_nosecret' => true]) ->will($this->returnValue('http://magento.com/catalog/product/edit/id/1')); - $this->assertEquals($this->rssFeed, $this->block->getRssData()); + + $data = $this->block->getRssData(); + $this->assertTrue(is_string($data['title'])); + $this->assertTrue(is_string($data['description'])); + $this->assertTrue(is_string($data['entries'][0]['description'])); + $this->assertEquals($this->rssFeed, $data); } public function testGetCacheLifetime() diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php new file mode 100644 index 0000000000000..7ed8b13fce750 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Block\Product\View; + +use Magento\Catalog\Block\Product\Context; +use Magento\Catalog\Block\Product\View\Gallery; +use Magento\Catalog\Block\Product\View\GalleryOptions; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\View\Config; +use Magento\Framework\Config\View; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GalleryOptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GalleryOptions + */ + private $model; + + /** + * @var Gallery|\PHPUnit_Framework_MockObject_MockObject + */ + private $gallery; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var View|\PHPUnit_Framework_MockObject_MockObject + */ + private $configView; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $viewConfig; + + /** + * @var Escaper + */ + private $escaper; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->escaper = $objectManager->getObject(Escaper::class); + $this->configView = $this->createMock(View::class); + + $this->viewConfig = $this->createConfiguredMock( + Config::class, + [ + 'getViewConfig' => $this->configView + ] + ); + + $this->context = $this->createConfiguredMock( + Context::class, + [ + 'getEscaper' => $this->escaper, + 'getViewConfig' => $this->viewConfig + ] + ); + + $this->gallery = $this->createMock(Gallery::class); + + $this->jsonSerializer = $objectManager->getObject( + Json::class + ); + + $this->model = $objectManager->getObject(GalleryOptions::class, [ + 'context' => $this->context, + 'jsonSerializer' => $this->jsonSerializer, + 'gallery' => $this->gallery + ]); + } + + public function testGetOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/nav', 'thumbs'], + ['Magento_Catalog', 'gallery/loop', false], + ['Magento_Catalog', 'gallery/keyboard', true], + ['Magento_Catalog', 'gallery/arrows', true], + ['Magento_Catalog', 'gallery/caption', false], + ['Magento_Catalog', 'gallery/allowfullscreen', true], + ['Magento_Catalog', 'gallery/navdir', 'horizontal'], + ['Magento_Catalog', 'gallery/navarrows', true], + ['Magento_Catalog', 'gallery/navtype', 'slides'], + ['Magento_Catalog', 'gallery/thumbmargin', '5'], + ['Magento_Catalog', 'gallery/transition/effect', 'slide'], + ['Magento_Catalog', 'gallery/transition/duration', '500'], + ]; + + $imageAttributesMap = [ + ['product_page_image_medium','height',null, 100], + ['product_page_image_medium','width',null, 200], + ['product_page_image_small','height',null, 300], + ['product_page_image_small','width',null, 400] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + $this->gallery->expects($this->any()) + ->method('getImageAttribute') + ->will($this->returnValueMap($imageAttributesMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertSame('thumbs', $decodedJson['nav']); + $this->assertSame(false, $decodedJson['loop']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['arrows']); + $this->assertSame(false, $decodedJson['showCaption']); + $this->assertSame(true, $decodedJson['allowfullscreen']); + $this->assertSame('horizontal', $decodedJson['navdir']); + $this->assertSame(true, $decodedJson['navarrows']); + $this->assertSame('slides', $decodedJson['navtype']); + $this->assertSame(5, $decodedJson['thumbmargin']); + $this->assertSame('slide', $decodedJson['transition']); + $this->assertSame(500, $decodedJson['transitionduration']); + $this->assertSame(100, $decodedJson['height']); + $this->assertSame(200, $decodedJson['width']); + $this->assertSame(300, $decodedJson['thumbheight']); + $this->assertSame(400, $decodedJson['thumbwidth']); + } + + public function testGetFSOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/nav', false], + ['Magento_Catalog', 'gallery/fullscreen/loop', true], + ['Magento_Catalog', 'gallery/fullscreen/keyboard', true], + ['Magento_Catalog', 'gallery/fullscreen/arrows', false], + ['Magento_Catalog', 'gallery/fullscreen/caption', true], + ['Magento_Catalog', 'gallery/fullscreen/navdir', 'vertical'], + ['Magento_Catalog', 'gallery/fullscreen/navarrows', false], + ['Magento_Catalog', 'gallery/fullscreen/navtype', 'thumbs'], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', '10'], + ['Magento_Catalog', 'gallery/fullscreen/transition/effect', 'dissolve'], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + //Note, this tests the special case for nav variable set to false. It + //Should not be converted to boolean. + $this->assertSame('false', $decodedJson['nav']); + $this->assertSame(true, $decodedJson['loop']); + $this->assertSame(false, $decodedJson['arrows']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['showCaption']); + $this->assertSame('vertical', $decodedJson['navdir']); + $this->assertSame(false, $decodedJson['navarrows']); + $this->assertSame(10, $decodedJson['thumbmargin']); + $this->assertSame('thumbs', $decodedJson['navtype']); + $this->assertSame('dissolve', $decodedJson['transition']); + $this->assertSame(300, $decodedJson['transitionduration']); + } + + public function testGetOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } + + public function testGetFSOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/keyboard', false], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('keyboard', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php index 45de62e218cfc..adf00333721ba 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php @@ -66,8 +66,8 @@ private function setObjectProperty($object, string $propertyName, $value) : void */ public function testExecute() : void { - $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2]; - $result = '{"id":3,"path":"1/2/3","parentId":"2"}'; + $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2, 'level' => 2]; + $result = '{"id":3,"path":"1/2/3","parentId":"2","level":"2"}'; $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php deleted file mode 100644 index de44af7f58afc..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Action\Attribute; - -/** - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save */ - protected $object; - - /** @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute|\PHPUnit_Framework_MockObject_MockObject */ - protected $attributeHelper; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $dataObjectHelperMock; - - /** @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockIndexerProcessor; - - /** @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; - - /** @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; - - /** @var \Magento\Framework\App\Response\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; - - /** @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManager; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $url; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirect; - - /** @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject */ - protected $actionFlag; - - /** @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $view; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManager; - - /** @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $session; - - /** @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $authorization; - - /** @var \Magento\Backend\Model\Auth|\PHPUnit_Framework_MockObject_MockObject */ - protected $auth; - - /** @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $helper; - - /** @var \Magento\Backend\Model\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $backendUrl; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $formKeyValidator; - - /** @var \Magento\Framework\Locale\ResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $localeResolver; - - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; - - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemService; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $stockItem; - - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockConfig; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemRepository; - - /** - * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectFactory; - - protected function setUp() - { - $this->attributeHelper = $this->createPartialMock( - \Magento\Catalog\Helper\Product\Edit\Action\Attribute::class, - ['getProductIds', 'getSelectedStoreId', 'getStoreWebsiteId'] - ); - - $this->dataObjectHelperMock = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->stockIndexerProcessor = $this->createPartialMock( - \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class, - ['reindexList'] - ); - - $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultRedirectFactory = $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->resultRedirectFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($resultRedirect); - - $this->prepareContext(); - - $this->object = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( - \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::class, - [ - 'context' => $this->context, - 'attributeHelper' => $this->attributeHelper, - 'stockIndexerProcessor' => $this->stockIndexerProcessor, - 'dataObjectHelper' => $this->dataObjectHelperMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function prepareContext() - { - $this->stockItemRepository = $this->getMockBuilder( - \Magento\CatalogInventory\Api\StockItemRepositoryInterface::class - )->disableOriginalConstructor()->getMock(); - - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); - $this->objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $this->actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); - $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); - $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); - $this->session = $this->createMock(\Magento\Backend\Model\Session::class); - $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class); - $this->auth = $this->createMock(\Magento\Backend\Model\Auth::class); - $this->helper = $this->createMock(\Magento\Backend\Helper\Data::class); - $this->backendUrl = $this->createMock(\Magento\Backend\Model\UrlInterface::class); - $this->formKeyValidator = $this->createMock(\Magento\Framework\Data\Form\FormKey\Validator::class); - $this->localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); - - $this->context = $this->context = $this->createPartialMock(\Magento\Backend\App\Action\Context::class, [ - 'getRequest', - 'getResponse', - 'getObjectManager', - 'getEventManager', - 'getUrl', - 'getRedirect', - 'getActionFlag', - 'getView', - 'getMessageManager', - 'getSession', - 'getAuthorization', - 'getAuth', - 'getHelper', - 'getBackendUrl', - 'getFormKeyValidator', - 'getLocaleResolver', - 'getResultRedirectFactory' - ]); - $this->context->expects($this->any())->method('getRequest')->willReturn($this->request); - $this->context->expects($this->any())->method('getResponse')->willReturn($this->response); - $this->context->expects($this->any())->method('getObjectManager')->willReturn($this->objectManager); - $this->context->expects($this->any())->method('getEventManager')->willReturn($this->eventManager); - $this->context->expects($this->any())->method('getUrl')->willReturn($this->url); - $this->context->expects($this->any())->method('getRedirect')->willReturn($this->redirect); - $this->context->expects($this->any())->method('getActionFlag')->willReturn($this->actionFlag); - $this->context->expects($this->any())->method('getView')->willReturn($this->view); - $this->context->expects($this->any())->method('getMessageManager')->willReturn($this->messageManager); - $this->context->expects($this->any())->method('getSession')->willReturn($this->session); - $this->context->expects($this->any())->method('getAuthorization')->willReturn($this->authorization); - $this->context->expects($this->any())->method('getAuth')->willReturn($this->auth); - $this->context->expects($this->any())->method('getHelper')->willReturn($this->helper); - $this->context->expects($this->any())->method('getBackendUrl')->willReturn($this->backendUrl); - $this->context->expects($this->any())->method('getFormKeyValidator')->willReturn($this->formKeyValidator); - $this->context->expects($this->any())->method('getLocaleResolver')->willReturn($this->localeResolver); - $this->context->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($this->resultRedirectFactory); - - $this->product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, - ['isProductsHasSku', '__wakeup'] - ); - - $this->stockItemService = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getStockItem', 'saveStockItem']) - ->getMockForAbstractClass(); - $this->stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) - ->setMethods(['getId', 'getProductId']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->stockConfig = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->objectManager->expects($this->any())->method('create')->will($this->returnValueMap([ - [\Magento\Catalog\Model\Product::class, [], $this->product], - [\Magento\CatalogInventory\Api\StockRegistryInterface::class, [], $this->stockItemService], - [\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class, [], $this->stockItemRepository], - ])); - - $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ - [\Magento\CatalogInventory\Api\StockConfigurationInterface::class, $this->stockConfig], - ])); - } - - public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() - { - $this->attributeHelper->expects($this->any())->method('getProductIds')->will($this->returnValue([5])); - $this->attributeHelper->expects($this->any())->method('getSelectedStoreId')->will($this->returnValue([1])); - $this->attributeHelper->expects($this->any())->method('getStoreWebsiteId')->will($this->returnValue(1)); - $this->stockConfig->expects($this->any())->method('getConfigItemOptions')->will($this->returnValue([])); - $this->dataObjectHelperMock->expects($this->any()) - ->method('populateWithArray') - ->with($this->stockItem, $this->anything(), \Magento\CatalogInventory\Api\Data\StockItemInterface::class) - ->willReturnSelf(); - $this->product->expects($this->any())->method('isProductsHasSku')->with([5])->will($this->returnValue(true)); - $this->stockItemService->expects($this->any())->method('getStockItem')->with(5, 1) - ->will($this->returnValue($this->stockItem)); - $this->stockIndexerProcessor->expects($this->any())->method('reindexList')->with([5]); - - $this->request->expects($this->any())->method('getParam')->will($this->returnValueMap([ - ['inventory', [], [7]], - ])); - - $this->messageManager->expects($this->never())->method('addErrorMessage'); - $this->messageManager->expects($this->never())->method('addExceptionMessage'); - - $this->object->execute(); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php index ced65b2d2e15d..30d3503e4640e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\Serialize\Serializer\FormData; use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; @@ -94,6 +95,11 @@ class SaveTest extends AttributeTest */ private $productAttributeMock; + /** + * @var AttributeCodeValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeCodeValidatorMock; + protected function setUp() { parent::setUp(); @@ -138,6 +144,9 @@ protected function setUp() $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) ->disableOriginalConstructor() ->getMock(); + $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) + ->disableOriginalConstructor() + ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) ->setMethods(['getId', 'get']) ->getMockForAbstractClass(); @@ -171,6 +180,7 @@ protected function getModel() 'groupCollectionFactory' => $this->groupCollectionFactoryMock, 'layoutFactory' => $this->layoutFactoryMock, 'formDataSerializer' => $this->formDataSerializerMock, + 'attributeCodeValidator' => $this->attributeCodeValidatorMock ]); } @@ -224,6 +234,10 @@ public function testExecute() $this->productAttributeMock ->method('getAttributeCode') ->willReturn('test_code'); + $this->attributeCodeValidatorMock + ->method('isValid') + ->with('test_code') + ->willReturn(true); $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn($data); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index c6210f93e1290..742148b1bf7f1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Validate; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\Serialize\Serializer\FormData; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; @@ -67,6 +68,11 @@ class ValidateTest extends AttributeTest */ private $formDataSerializerMock; + /** + * @var AttributeCodeValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeCodeValidatorMock; + protected function setUp() { parent::setUp(); @@ -95,6 +101,9 @@ protected function setUp() $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) ->disableOriginalConstructor() ->getMock(); + $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) + ->disableOriginalConstructor() + ->getMock(); $this->contextMock->expects($this->any()) ->method('getObjectManager') @@ -117,6 +126,7 @@ protected function getModel() 'layoutFactory' => $this->layoutFactoryMock, 'multipleAttributeList' => ['select' => 'option'], 'formDataSerializer' => $this->formDataSerializerMock, + 'attributeCodeValidator' => $this->attributeCodeValidatorMock, ] ); } @@ -141,6 +151,12 @@ public function testExecute() $this->attributeMock->expects($this->once()) ->method('loadByCode') ->willReturnSelf(); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + $this->requestMock->expects($this->once()) ->method('has') ->with('new_attribute_set_name') @@ -190,6 +206,11 @@ public function testUniqueValidation(array $options, $isError) ->with($serializedOptions) ->willReturn($options); + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($this->attributeMock); @@ -333,6 +354,11 @@ public function testEmptyOption(array $options, $result) ->method('loadByCode') ->willReturnSelf(); + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + $this->resultJsonFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->resultJson); @@ -444,6 +470,10 @@ public function testExecuteWithOptionsDataError() [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] ]); + $this->attributeCodeValidatorMock + ->method('isValid') + ->willReturn(true); + $this->attributeMock ->method('loadByCode') ->willReturnSelf(); @@ -463,4 +493,81 @@ public function testExecuteWithOptionsDataError() $this->getModel()->execute(); } + + /** + * Test execute with an invalid attribute code + * + * @dataProvider provideInvalidAttributeCodes + * @param string $attributeCode + * @param $result + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithInvalidAttributeCode($attributeCode, $result) + { + $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['frontend_label', null, null], + ['frontend_input', 'select', 'multipleselect'], + ['attribute_code', null, $attributeCode], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], + ]); + + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn(["key" => "value"]); + + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); + + $this->attributeMock->expects($this->once()) + ->method('loadByCode') + ->willReturnSelf(); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with($attributeCode) + ->willReturn(false); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('getMessages') + ->willReturn(['Invalid Attribute Code.']); + + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->willReturnArgument(0); + + $response = $this->getModel()->execute(); + $responseObject = json_decode($response); + + $this->assertEquals($responseObject, $result); + } + + /** + * Providing invalid attribute codes + * + * @return array + */ + public function provideInvalidAttributeCodes() + { + return [ + 'invalid attribute code' => [ + '.attribute_code', + (object) [ + 'error' => true, + 'message' => 'Invalid Attribute Code.', + ] + ] + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php index d93520297e485..60c6f2f1bd821 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php @@ -124,7 +124,7 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $this->pageConfig->expects($this->any())->method('addBodyClass')->will($this->returnSelf()); - $this->page = $this->getMockBuilder(\Magento\Framework\View\Page::class) + $this->page = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) ->setMethods(['getConfig', 'initLayout', 'addPageLayoutHandles', 'getLayout', 'addUpdate']) ->disableOriginalConstructor()->getMock(); $this->page->expects($this->any())->method('getConfig')->will($this->returnValue($this->pageConfig)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php index 9fb2adb2b8ecd..97c098ba0ff2e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php @@ -43,6 +43,11 @@ class TreeTest extends \PHPUnit\Framework\TestCase */ protected $node; + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\TreeFactory + */ + private $treeResourceFactoryMock; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -59,6 +64,12 @@ protected function setUp() \Magento\Store\Model\StoreManagerInterface::class )->disableOriginalConstructor()->getMock(); + $this->treeResourceFactoryMock = $this->createMock( + \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class + ); + $this->treeResourceFactoryMock->method('create') + ->willReturn($this->categoryTreeMock); + $methods = ['create']; $this->treeFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory::class, $methods); @@ -70,7 +81,8 @@ protected function setUp() 'categoryCollection' => $this->categoryCollection, 'categoryTree' => $this->categoryTreeMock, 'storeManager' => $this->storeManagerMock, - 'treeFactory' => $this->treeFactoryMock + 'treeFactory' => $this->treeFactoryMock, + 'treeResourceFactory' => $this->treeResourceFactoryMock, ] ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 64eedbce2d982..b4042d6b02c13 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -383,13 +383,16 @@ public function testReindexFlatEnabled( public function reindexFlatDisabledTestDataProvider() { return [ - [false, null, null, null, 0], - [true, null, null, null, 0], - [false, [], null, null, 0], - [false, ["1", "2"], null, null, 1], - [false, null, 1, null, 1], - [false, ["1", "2"], 0, 1, 1], - [false, null, 1, 1, 0], + [false, null, null, null, null, null, 0], + [true, null, null, null, null, null, 0], + [false, [], null, null, null, null, 0], + [false, ["1", "2"], null, null, null, null, 1], + [false, null, 1, null, null, null, 1], + [false, ["1", "2"], 0, 1, null, null, 1], + [false, null, 1, 1, null, null, 0], + [false, ["1", "2"], null, null, 0, 1, 1], + [false, ["1", "2"], null, null, 1, 0, 1], + ]; } @@ -407,11 +410,16 @@ public function testReindexFlatDisabled( $affectedIds, $isAnchorOrig, $isAnchor, + $isActiveOrig, + $isActive, $expectedProductReindexCall ) { $this->category->setAffectedProductIds($affectedIds); $this->category->setData('is_anchor', $isAnchor); $this->category->setOrigData('is_anchor', $isAnchorOrig); + $this->category->setData('is_active', $isActive); + $this->category->setOrigData('is_active', $isActiveOrig); + $this->category->setAffectedProductIds($affectedIds); $pathIds = ['path/1/2', 'path/2/3']; @@ -422,7 +430,7 @@ public function testReindexFlatDisabled( ->method('isFlatEnabled') ->will($this->returnValue(false)); - $this->productIndexer->expects($this->exactly(1)) + $this->productIndexer ->method('isScheduled') ->willReturn($productScheduled); $this->productIndexer->expects($this->exactly($expectedProductReindexCall)) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php index 5b1d3bf7943fc..23f0aec5b69a2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php @@ -9,6 +9,11 @@ use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Tests \Magento\Catalog\Model\Config\CatalogClone\Media\Image. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ImageTest extends \PHPUnit\Framework\TestCase { /** @@ -36,6 +41,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $attribute; + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) @@ -62,54 +75,79 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->getMockBuilder( + \Magento\Framework\Escaper::class + ) + ->disableOriginalConstructor() + ->setMethods(['escapeHtml']) + ->getMock(); + $helper = new ObjectManager($this); $this->model = $helper->getObject( \Magento\Catalog\Model\Config\CatalogClone\Media\Image::class, [ 'eavConfig' => $this->eavConfig, - 'attributeCollectionFactory' => $this->attributeCollectionFactory + 'attributeCollectionFactory' => $this->attributeCollectionFactory, + 'escaper' => $this->escaperMock, ] ); } - public function testGetPrefixes() + /** + * @param string $actualLabel + * @param string $expectedLabel + * @return void + * + * @dataProvider getPrefixesDataProvider + */ + public function testGetPrefixes(string $actualLabel, string $expectedLabel): void { $entityTypeId = 3; /** @var \Magento\Eav\Model\Entity\Type|\PHPUnit_Framework_MockObject_MockObject $entityType */ $entityType = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) ->disableOriginalConstructor() ->getMock(); - $entityType->expects($this->once())->method('getId')->will($this->returnValue($entityTypeId)); + $entityType->expects($this->once())->method('getId')->willReturn($entityTypeId); /** @var AbstractFrontend|\PHPUnit_Framework_MockObject_MockObject $frontend */ $frontend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class) ->setMethods(['getLabel']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $frontend->expects($this->once())->method('getLabel')->will($this->returnValue('testLabel')); + $frontend->expects($this->once())->method('getLabel')->willReturn($actualLabel); - $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with( - $this->equalTo($entityTypeId) - ); - $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with( - $this->equalTo('media_image') - ); + $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with($entityTypeId); + $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with('media_image'); - $this->attribute->expects($this->once())->method('getAttributeCode')->will( - $this->returnValue('attributeCode') - ); - $this->attribute->expects($this->once())->method('getFrontend')->will( - $this->returnValue($frontend) - ); + $this->attribute->expects($this->once())->method('getAttributeCode')->willReturn('attributeCode'); + $this->attribute->expects($this->once())->method('getFrontend')->willReturn($frontend); - $this->attributeCollection->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$this->attribute])) - ); + $this->attributeCollection->expects($this->any())->method('getIterator') + ->willReturn(new \ArrayIterator([$this->attribute])); + + $this->eavConfig->expects($this->any())->method('getEntityType')->with(Product::ENTITY) + ->willReturn($entityType); - $this->eavConfig->expects($this->any())->method('getEntityType')->with( - $this->equalTo(Product::ENTITY) - )->will($this->returnValue($entityType)); + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($actualLabel) + ->willReturn($expectedLabel); - $this->assertEquals([['field' => 'attributeCode_', 'label' => 'testLabel']], $this->model->getPrefixes()); + $this->assertEquals([['field' => 'attributeCode_', 'label' => $expectedLabel]], $this->model->getPrefixes()); + } + + /** + * @return array + */ + public function getPrefixesDataProvider(): array + { + return [ + [ + 'actual_label' => 'testLabel', + 'expected_label' => 'testLabel', + ], + [ + 'actual_label' => '<media-image-attributelabel', + 'expected_label' => '<media-image-attributelabel', + ], + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php index 90c3f999a6a8b..2e1cff834fd34 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php @@ -3,15 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\Action\Full; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use PHPUnit\Framework\MockObject\MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -19,45 +33,50 @@ class FullTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full|\PHPUnit_Framework_MockObject_MockObject + * @var Full|MockObject */ private $model; /** - * @var DecimalFactory|\PHPUnit_Framework_MockObject_MockObject + * @var DecimalFactory|MockObject */ private $eavDecimalFactory; /** - * @var SourceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var SourceFactory|MockObject */ private $eavSourceFactory; /** - * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + * @var MetadataPool|MockObject */ private $metadataPool; /** - * @var BatchProviderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var BatchProviderInterface|MockObject */ private $batchProvider; /** - * @var BatchSizeCalculator|\PHPUnit_Framework_MockObject_MockObject + * @var BatchSizeCalculator|MockObject */ private $batchSizeCalculator; /** - * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var ActiveTableSwitcher|MockObject */ private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|MockObject */ private $scopeConfig; + /** + * @var Generator + */ + private $batchQueryGenerator; + /** * @return void */ @@ -67,15 +86,16 @@ protected function setUp() $this->eavSourceFactory = $this->createPartialMock(SourceFactory::class, ['create']); $this->metadataPool = $this->createMock(MetadataPool::class); $this->batchProvider = $this->getMockForAbstractClass(BatchProviderInterface::class); + $this->batchQueryGenerator = $this->createMock(Generator::class); $this->batchSizeCalculator = $this->createMock(BatchSizeCalculator::class); $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( - \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full::class, + Full::class, [ 'eavDecimalFactory' => $this->eavDecimalFactory, 'eavSourceFactory' => $this->eavSourceFactory, @@ -83,7 +103,8 @@ protected function setUp() 'batchProvider' => $this->batchProvider, 'batchSizeCalculator' => $this->batchSizeCalculator, 'activeTableSwitcher' => $this->activeTableSwitcher, - 'scopeConfig' => $this->scopeConfig + 'scopeConfig' => $this->scopeConfig, + 'batchQueryGenerator' => $this->batchQueryGenerator, ] ); } @@ -96,15 +117,15 @@ public function testExecute() $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(1); $ids = [1, 2, 3]; - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + $connectionMock = $this->getMockBuilder(AdapterInterface::class) ->getMockForAbstractClass(); $connectionMock->expects($this->atLeastOnce())->method('describeTable')->willReturn(['id' => []]); - $eavSource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::class) + $eavSource = $this->getMockBuilder(Source::class) ->disableOriginalConstructor() ->getMock(); - $eavDecimal = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal::class) + $eavDecimal = $this->getMockBuilder(Decimal::class) ->disableOriginalConstructor() ->getMock(); @@ -125,22 +146,28 @@ public function testExecute() $this->eavSourceFactory->expects($this->once())->method('create')->will($this->returnValue($eavDecimal)); - $entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + $entityMetadataMock = $this->getMockBuilder(EntityMetadataInterface::class) ->getMockForAbstractClass(); $this->metadataPool->expects($this->atLeastOnce()) ->method('getMetadata') - ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->with(ProductInterface::class) ->willReturn($entityMetadataMock); - $this->batchProvider->expects($this->atLeastOnce()) - ->method('getBatches') - ->willReturn([['from' => 10, 'to' => 100]]); - $this->batchProvider->expects($this->atLeastOnce()) - ->method('getBatchIds') + // Super inefficient algorithm in some cases + $this->batchProvider->expects($this->never()) + ->method('getBatches'); + + $batchQuery = $this->createMock(Select::class); + + $connectionMock->method('fetchCol') + ->with($batchQuery) ->willReturn($ids); - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + $this->batchQueryGenerator->method('generate') + ->willReturn([$batchQuery]); + + $selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() ->getMock(); @@ -153,7 +180,7 @@ public function testExecute() /** * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function testExecuteWithDisabledEavIndexer() { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php index e9eee5c766883..80b6db2a516bd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php @@ -6,10 +6,12 @@ namespace Magento\Catalog\Test\Unit\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; -use \Magento\Catalog\Model\Product\Copier; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Copier; /** + * Test for Magento\Catalog\Model\Product\Copier class. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CopierTest extends \PHPUnit\Framework\TestCase @@ -76,6 +78,9 @@ protected function setUp() ]); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testCopy() { $stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) @@ -103,8 +108,44 @@ public function testCopy() ['linkField', null, '1'], ]); - $resourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - $this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock)); + $entityMock = $this->getMockForAbstractClass( + \Magento\Eav\Model\Entity\AbstractEntity::class, + [], + '', + false, + true, + true, + ['checkAttributeUniqueValue'] + ); + $entityMock->expects($this->any()) + ->method('checkAttributeUniqueValue') + ->willReturn(true); + + $attributeMock = $this->getMockForAbstractClass( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + [], + '', + false, + true, + true, + ['getEntity'] + ); + $attributeMock->expects($this->any()) + ->method('getEntity') + ->willReturn($entityMock); + + $resourceMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute']) + ->getMock(); + $resourceMock->expects($this->any()) + ->method('getAttributeRawValue') + ->willReturn('urk-key-1'); + $resourceMock->expects($this->any()) + ->method('getAttribute') + ->willReturn($attributeMock); + + $this->productMock->expects($this->any())->method('getResource')->will($this->returnValue($resourceMock)); $duplicateMock = $this->createPartialMock( Product::class, @@ -119,11 +160,11 @@ public function testCopy() 'setCreatedAt', 'setUpdatedAt', 'setId', - 'setStoreId', 'getEntityId', 'save', 'setUrlKey', - 'getUrlKey', + 'setStoreId', + 'getStoreIds', ] ); $this->productFactoryMock->expects($this->once())->method('create')->will($this->returnValue($duplicateMock)); @@ -138,19 +179,13 @@ public function testCopy() )->with( \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED ); + $duplicateMock->expects($this->atLeastOnce())->method('setStoreId'); $duplicateMock->expects($this->once())->method('setCreatedAt')->with(null); $duplicateMock->expects($this->once())->method('setUpdatedAt')->with(null); $duplicateMock->expects($this->once())->method('setId')->with(null); - $duplicateMock->expects( - $this->once() - )->method( - 'setStoreId' - )->with( - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); + $duplicateMock->expects($this->atLeastOnce())->method('getStoreIds')->willReturn([]); $duplicateMock->expects($this->atLeastOnce())->method('setData')->willReturn($duplicateMock); $this->copyConstructorMock->expects($this->once())->method('build')->with($this->productMock, $duplicateMock); - $duplicateMock->expects($this->once())->method('getUrlKey')->willReturn('urk-key-1'); $duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2')->willReturn($duplicateMock); $duplicateMock->expects($this->once())->method('save'); @@ -158,7 +193,8 @@ public function testCopy() $duplicateMock->expects($this->any())->method('getData')->willReturnMap([ ['linkField', null, '2'], - ]); $this->optionRepositoryMock->expects($this->once()) + ]); + $this->optionRepositoryMock->expects($this->once()) ->method('duplicate') ->with($this->productMock, $duplicateMock); $resourceMock->expects($this->once())->method('duplicate')->with(1, 2); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php index da6b790fedfa6..7c2ec8abb768a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php @@ -18,6 +18,11 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,8 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); + $config = [ [ 'label' => 'group label 1', @@ -51,7 +58,8 @@ protected function setUp() $configMock->expects($this->once())->method('getAll')->will($this->returnValue($config)); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\DefaultValidator( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -63,10 +71,10 @@ public function isValidTitleDataProvider() { $mess = ['option required fields' => 'Missed values for option required fields']; return [ - ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], - ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), [], true], - [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], - [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], + ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], + ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), [], true], + [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], + [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], ]; } @@ -79,15 +87,18 @@ public function isValidTitleDataProvider() * @param bool $result * @dataProvider isValidTitleDataProvider */ - public function testIsValidTitle($title, $type, $priceType, $product, $messages, $result) + public function testIsValidTitle($title, $type, $priceType, $price, $product, $messages, $result) { - $methods = ['getTitle', 'getType', 'getPriceType', '__wakeup', 'getProduct']; + $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); $valueMock->expects($this->any())->method('getType')->will($this->returnValue($type)); $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); - // $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); + $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); + + $this->localeFormatMock->expects($this->once())->method('getNumber')->will($this->returnValue($price)); + $this->assertEquals($result, $this->validator->isValid($valueMock)); $this->assertEquals($messages, $this->validator->getMessages()); } @@ -126,4 +137,43 @@ public function testIsValidFail($product) $this->assertFalse($this->validator->isValid($valueMock)); $this->assertEquals($messages, $this->validator->getMessages()); } + + /** + * Data provider for testValidationNegativePrice + * @return array + */ + public function validationPriceDataProvider() + { + return [ + ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 1])], + ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 0])], + ['option_title', 'name 1.1', 'fixed', 12, new \Magento\Framework\DataObject(['store_id' => 1])], + ['option_title', 'name 1.1', 'fixed', 12, new \Magento\Framework\DataObject(['store_id' => 0])] + ]; + } + + /** + * @param $title + * @param $type + * @param $priceType + * @param $price + * @param $product + * @dataProvider validationPriceDataProvider + */ + public function testValidationPrice($title, $type, $priceType, $price, $product) + { + $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; + $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); + $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); + $valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue($type)); + $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); + $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); + $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); + + $this->localeFormatMock->expects($this->once())->method('getNumber')->will($this->returnValue($price)); + + $messages = []; + $this->assertTrue($this->validator->isValid($valueMock)); + $this->assertEquals($messages, $this->validator->getMessages()); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php index 2de993c075514..e688da1c6aa16 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php @@ -18,6 +18,11 @@ class FileTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,8 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); + $config = [ [ 'label' => 'group label 1', @@ -53,7 +60,8 @@ protected function setUp() $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\File( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -70,6 +78,15 @@ public function testIsValidSuccess() ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(15)); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(2)) + ->method('getNumber') + ->with($this->equalTo(15)) + ->will($this->returnValue(15)); $this->assertEmpty($this->validator->getMessages()); $this->assertTrue($this->validator->isValid($this->valueMock)); } @@ -87,6 +104,16 @@ public function testIsValidWithNegativeImageSize() ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(-10)); $this->valueMock->expects($this->never())->method('getImageSizeY'); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(1)) + ->method('getNumber') + ->with($this->equalTo(-10)) + ->will($this->returnValue(-10)); + $messages = [ 'option values' => 'Invalid option value', ]; @@ -107,6 +134,15 @@ public function testIsValidWithNegativeImageSizeY() ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(-10)); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(2)) + ->method('getNumber') + ->with($this->equalTo(-10)) + ->will($this->returnValue(-10)); $messages = [ 'option values' => 'Invalid option value', ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php index b97783edf856c..7fad5592a2d21 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php @@ -18,6 +18,11 @@ class SelectTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,7 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); $config = [ [ 'label' => 'group label 1', @@ -53,7 +59,8 @@ protected function setUp() $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods, []); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\Select( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -69,6 +76,12 @@ public function testIsValidSuccess($expectedResult, array $value) $this->valueMock->expects($this->never())->method('getPriceType'); $this->valueMock->expects($this->never())->method('getPrice'); $this->valueMock->expects($this->any())->method('getData')->with('values')->will($this->returnValue([$value])); + if (isset($value['price'])) { + $this->localeFormatMock + ->expects($this->once()) + ->method('getNumber') + ->will($this->returnValue($value['price'])); + } $this->assertEquals($expectedResult, $this->validator->isValid($this->valueMock)); } @@ -117,6 +130,7 @@ public function testIsValidateWithInvalidOptionValues() ->method('getData') ->with('values') ->will($this->returnValue('invalid_data')); + $messages = [ 'option values' => 'Invalid option value', ]; @@ -159,6 +173,7 @@ public function testIsValidateWithInvalidData($priceType, $price, $title) $this->valueMock->expects($this->never())->method('getPriceType'); $this->valueMock->expects($this->never())->method('getPrice'); $this->valueMock->expects($this->any())->method('getData')->with('values')->will($this->returnValue([$value])); + $this->localeFormatMock->expects($this->any())->method('getNumber')->will($this->returnValue($price)); $messages = [ 'option values' => 'Invalid option value', ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php index 4881154728ddc..a3e6189f74925 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php @@ -18,6 +18,11 @@ class TextTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,7 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); $config = [ [ 'label' => 'group label 1', @@ -53,7 +59,8 @@ protected function setUp() $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\Text( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -69,6 +76,10 @@ public function testIsValidSuccess() $this->valueMock->method('getPrice') ->willReturn(10); $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(10)); + $this->localeFormatMock->expects($this->exactly(2)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); $this->assertTrue($this->validator->isValid($this->valueMock)); $this->assertEmpty($this->validator->getMessages()); } @@ -85,6 +96,15 @@ public function testIsValidWithNegativeMaxCharacters() $this->valueMock->method('getPrice') ->willReturn(10); $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(-10)); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(1)) + ->method('getNumber') + ->with($this->equalTo(-10)) + ->will($this->returnValue(-10)); $messages = [ 'option values' => 'Invalid option value', ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php index fce4a02622d9e..38bed83cb9504 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php @@ -80,6 +80,7 @@ protected function setUp() public function testFilterProductActions() { + $typeId = 'recently_compared_product'; $productsData = [ 1 => [ 'added_at' => 12, @@ -87,7 +88,7 @@ public function testFilterProductActions() ], 2 => [ 'added_at' => 13, - 'product_id' => 2, + 'product_id' => '2', ], 3 => [ 'added_at' => 14, @@ -126,10 +127,12 @@ public function testFilterProductActions() $collection->expects($this->once()) ->method('addFilterByUserIdentities') ->with(1, 34); - $collection->expects($this->any()) + $collection->expects($this->at(1)) ->method('addFieldToFilter') - ->withConsecutive(['type_id'], ['product_id']); - + ->with('type_id', $typeId); + $collection->expects($this->at(2)) + ->method('addFieldToFilter') + ->with('product_id', [1, 2]); $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); $collection->expects($this->once()) ->method('getIterator') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php index 034b04b6a757d..cfb54c3aefd0f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php @@ -29,12 +29,12 @@ ], ], 'renderer_attribute_with_invalid_value' => [ - '<?xml version="1.0"?><config><option name="name_one" renderer="true12"><inputType name="name_one"/>' . + '<?xml version="1.0"?><config><option name="name_one" renderer="123true"><inputType name="name_one"/>' . '</option></config>', [ - "Element 'option', attribute 'renderer': [facet 'pattern'] The value 'true12' is not accepted by the " . - "pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", - "Element 'option', attribute 'renderer': 'true12' is not a valid value of the atomic" . + "Element 'option', attribute 'renderer': [facet 'pattern'] The value '123true' is not accepted by the " . + "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'option', attribute 'renderer': '123true' is not a valid value of the atomic" . " type 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 9867fdf910219..22ba6bfa9f7fd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -8,11 +8,11 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Catalog\Model\Product\Attribute\Source\Status; /** * Product Test @@ -180,7 +180,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $extensionAttrbutes; + private $extensionAttributes; /** * @var \PHPUnit_Framework_MockObject_MockObject @@ -200,7 +200,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * @var ProductExtensionInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $extensionAttributes; + private $productExtAttributes; /** * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject @@ -218,7 +218,7 @@ protected function setUp() \Magento\Framework\Module\Manager::class, ['isEnabled'] ); - $this->extensionAttrbutes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) + $this->extensionAttributes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) ->setMethods(['getWebsiteIds', 'setWebsiteIds']) ->disableOriginalConstructor() ->getMock(); @@ -372,13 +372,13 @@ protected function setUp() $this->mediaConfig = $this->createMock(\Magento\Catalog\Model\Product\Media\Config::class); $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class); - $this->extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) + $this->productExtAttributes = $this->getMockBuilder(ProductExtensionInterface::class) ->setMethods(['getStockItem']) ->getMockForAbstractClass(); $this->extensionAttributesFactory ->expects($this->any()) ->method('create') - ->willReturn($this->extensionAttributes); + ->willReturn($this->productExtAttributes); $this->filterCustomAttribute = $this->createTestProxy( \Magento\Catalog\Model\FilterProductCustomAttribute::class @@ -567,14 +567,6 @@ public function testGetCategoryId() $this->assertEquals(10, $this->model->getCategoryId()); } - public function testGetCategoryIdWhenProductNotInCurrentCategory() - { - $this->model->setData('category_ids', [12]); - $this->category->expects($this->once())->method('getId')->will($this->returnValue(10)); - $this->registry->expects($this->any())->method('registry')->will($this->returnValue($this->category)); - $this->assertFalse($this->model->getCategoryId()); - } - public function testGetIdBySku() { $this->resource->expects($this->once())->method('getIdBySku')->will($this->returnValue(5)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php index e1847bea53fcb..868252da8190c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php @@ -23,7 +23,7 @@ '<?xml version="1.0"?><config><type name="some_name" modelInstance="123" /></config>', [ "Element 'type', attribute 'modelInstance': [facet 'pattern'] The value '123' is not accepted by the" . - " pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + " pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'type', attribute 'modelInstance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -57,7 +57,7 @@ '<?xml version="1.0"?><config><type name="some_name"><priceModel instance="123123" /></type></config>', [ "Element 'priceModel', attribute 'instance': [facet 'pattern'] The value '123123' is not accepted " . - "by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'priceModel', attribute 'instance': '123123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -66,7 +66,7 @@ '<?xml version="1.0"?><config><type name="some_name"><indexerModel instance="123" /></type></config>', [ "Element 'indexerModel', attribute 'instance': [facet 'pattern'] The value '123' is not accepted by " . - "the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'indexerModel', attribute 'instance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -83,7 +83,7 @@ '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel instance="1234"/></type></config>', [ "Element 'stockIndexerModel', attribute 'instance': [facet 'pattern'] The value '1234' is not " . - "accepted by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'stockIndexerModel', attribute 'instance': '1234' is not a valid value of the atomic " . "type 'modelName'.\nLine: 1\n" ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml index 7edbc399a9476..701338774baa5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml @@ -15,6 +15,14 @@ <stockIndexerModel instance="instance_name"/> </type> <type label="some_label" name="some_name2" modelInstance="model_name"> + <allowedSelectionTypes> + <type name="some_name" /> + </allowedSelectionTypes> + <priceModel instance="instance_name_with_digits_123" /> + <indexerModel instance="instance_name_with_digits_123" /> + <stockIndexerModel instance="instance_name_with_digits_123"/> + </type> + <type label="some_label" name="some_name3" modelInstance="model_name"> <allowedSelectionTypes> <type name="some_name" /> </allowedSelectionTypes> @@ -25,5 +33,6 @@ <composableTypes> <type name="some_name"/> <type name="some_name2"/> + <type name="some_name3"/> </composableTypes> </config> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index bb39aa7f9db77..0316b2e374d2f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -125,32 +125,25 @@ protected function setUp() $groupManagement = $this->getMockBuilder(\Magento\Customer\Api\GroupManagementInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->setMethods(['getId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); - $this->entityMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) ->disableOriginalConstructor() ->getMock(); - $this->galleryResourceMock = $this->getMockBuilder( \Magento\Catalog\Model\ResourceModel\Product\Gallery::class )->disableOriginalConstructor()->getMock(); - $this->metadataPoolMock = $this->getMockBuilder( \Magento\Framework\EntityManager\MetadataPool::class )->disableOriginalConstructor()->getMock(); - $this->galleryReadHandlerMock = $this->getMockBuilder( \Magento\Catalog\Model\Product\Gallery\ReadHandler::class )->disableOriginalConstructor()->getMock(); - $this->storeManager->expects($this->any())->method('getId')->willReturn(1); $this->storeManager->expects($this->any())->method('getStore')->willReturnSelf(); $universalFactory->expects($this->exactly(1))->method('create')->willReturnOnConsecutiveCalls( @@ -323,7 +316,7 @@ public function testAddTierPriceDataByGroupId() [ '(customer_group_id=? AND all_groups=0) OR all_groups=1', $customerGroupId] ) ->willReturnSelf(); - $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); + $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); $this->connectionMock->expects($this->once()) ->method('fetchAll') ->with($select) @@ -375,7 +368,7 @@ public function testAddTierPriceData() $select->expects($this->exactly(1))->method('where') ->with('entity_id IN(?)', [1]) ->willReturnSelf(); - $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); + $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); $this->connectionMock->expects($this->once()) ->method('fetchAll') ->with($select) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php b/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php index 37b0e15cac656..e225ec0daef6e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php @@ -11,7 +11,41 @@ "type" => "swatch_thumb", "width" => 75, "height" => 75, - "background" => [255, 25, 2] + "constrain" => false, + "aspect_ratio" => false, + "frame" => false, + "transparency" => false, + "background" => [255, 25, 2], + ], + "swatch_thumb_medium" => [ + "type" => "swatch_medium", + "width" => 750, + "height" => 750, + "constrain" => true, + "aspect_ratio" => true, + "frame" => true, + "transparency" => true, + "background" => [255, 25, 2], + ], + "swatch_thumb_large" => [ + "type" => "swatch_large", + "width" => 1080, + "height" => 720, + "constrain" => false, + "aspect_ratio" => false, + "frame" => false, + "transparency" => false, + "background" => [255, 25, 2], + ], + "swatch_thumb_small" => [ + "type" => "swatch_small", + "width" => 100, + "height" => 100, + "constrain" => true, + "aspect_ratio" => true, + "frame" => true, + "transparency" => true, + "background" => [255, 25, 2], ] ] ] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml b/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml index 253abc5e2e485..ee4ddaad53421 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml +++ b/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml @@ -11,6 +11,37 @@ <image id="swatch_thumb_base" type="swatch_thumb"> <width>75</width> <height>75</height> + <constrain>false</constrain> + <aspect_ratio>false</aspect_ratio> + <frame>false</frame> + <transparency>false</transparency> + <background>[255, 25, 2]</background> + </image> + <image id="swatch_thumb_medium" type="swatch_medium"> + <width>750</width> + <height>750</height> + <constrain>true</constrain> + <aspect_ratio>true</aspect_ratio> + <frame>true</frame> + <transparency>true</transparency> + <background>[255, 25, 2]</background> + </image> + <image id="swatch_thumb_large" type="swatch_large"> + <width>1080</width> + <height>720</height> + <constrain>0</constrain> + <aspect_ratio>0</aspect_ratio> + <frame>0</frame> + <transparency>0</transparency> + <background>[255, 25, 2]</background> + </image> + <image id="swatch_thumb_small" type="swatch_small"> + <width>100</width> + <height>100</height> + <constrain>1</constrain> + <aspect_ratio>1</aspect_ratio> + <frame>1</frame> + <transparency>1</transparency> <background>[255, 25, 2]</background> </image> </images> diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php new file mode 100644 index 0000000000000..774edcfeb6b64 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component; + +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Ui\Component\ColumnFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\ColumnInterface; +use Magento\Ui\Component\Filters\FilterModifier; + +/** + * ColumnFactory test. + */ +class ColumnFactoryTest extends TestCase +{ + /** + * @var ColumnFactory + */ + private $columnFactory; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductAttributeInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $attribute; + + /** + * @var ContextInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $context; + + /** + * @var UiComponentFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $uiComponentFactory; + + /** + * @var ColumnInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $column; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['usesSource']) + ->getMockForAbstractClass(); + $this->context = $this->createMock(ContextInterface::class); + $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); + $this->column = $this->getMockForAbstractClass(ColumnInterface::class); + $this->uiComponentFactory->method('create') + ->willReturn($this->column); + + $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ + 'componentFactory' => $this->uiComponentFactory + ]); + } + + /** + * Tests the create method will return correct object. + * + * @return void + */ + public function testCreatedObject(): void + { + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn([]); + + $object = $this->columnFactory->create($this->attribute, $this->context); + $this->assertEquals( + $this->column, + $object, + 'Object must be the same which the ui component factory creates.' + ); + } + + /** + * Tests create method with not filterable in grid attribute. + * + * @param array $filterModifiers + * @param null|string $filter + * + * @return void + * @dataProvider filterModifiersProvider + */ + public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, ?string $filter): void + { + $componentFactoryArgument = [ + 'data' => [ + 'config' => [ + 'label' => __(null), + 'dataType' => 'text', + 'add_field' => true, + 'visible' => null, + 'filter' => $filter, + 'component' => 'Magento_Ui/js/grid/columns/column', + ], + ], + 'context' => $this->context, + ]; + + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn($filterModifiers); + $this->attribute->method('getIsFilterableInGrid') + ->willReturn(false); + $this->attribute->method('getAttributeCode') + ->willReturn('color'); + + $this->uiComponentFactory->expects($this->once()) + ->method('create') + ->with($this->anything(), $this->anything(), $componentFactoryArgument); + + $this->columnFactory->create($this->attribute, $this->context); + } + + /** + * Filter modifiers data provider. + * + * @return array + */ + public function filterModifiersProvider(): array + { + return [ + 'without' => [ + 'filter_modifiers' => [], + 'filter' => null, + ], + 'with' => [ + 'filter_modifiers' => [ + 'color' => [ + 'condition_type' => 'notnull', + ], + ], + 'filter' => 'text', + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index cd6565f32ed18..a2d81854607a0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -154,38 +154,4 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } - - public function testModifyMetaWithCaching() - { - $this->arrayManagerMock->expects($this->exactly(2)) - ->method('findPath') - ->willReturn(true); - $cacheManager = $this->getMockBuilder(CacheInterface::class) - ->getMockForAbstractClass(); - $cacheManager->expects($this->once()) - ->method('load') - ->with(Categories::CATEGORY_TREE_ID . '_'); - $cacheManager->expects($this->once()) - ->method('save'); - - $modifier = $this->createModel(); - $cacheContextProperty = new \ReflectionProperty( - Categories::class, - 'cacheManager' - ); - $cacheContextProperty->setAccessible(true); - $cacheContextProperty->setValue($modifier, $cacheManager); - - $groupCode = 'test_group_code'; - $meta = [ - $groupCode => [ - 'children' => [ - 'category_ids' => [ - 'sortOrder' => 10, - ], - ], - ], - ]; - $modifier->modifyMeta($meta); - } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php index 6d7c8814bd474..0e0cb676cdf3e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php @@ -54,7 +54,16 @@ protected function setUp() ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(AbstractCollection::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getSelect', 'getTable', 'getIterator', 'isLoaded', 'toArray', 'getSize']) + ->setMethods([ + 'load', + 'getSelect', + 'getTable', + 'getIterator', + 'isLoaded', + 'toArray', + 'getSize', + 'setStoreId' + ]) ->getMockForAbstractClass(); $this->dbSelectMock = $this->getMockBuilder(DbSelect::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php index dbf1292e57368..a4ccaffc8fb6a 100644 --- a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php @@ -152,21 +152,21 @@ public function productJsonEncodeDataProvider() : array return [ [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test ™']]), - '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test \u2122"}}', + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test \u2122"}}', ], [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test "']]), - '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test ""}}', + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test ""}}', ], [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test <b>x</b>']]), - '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":' + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":' . '"Test <b>x<\/b>"}}', ], [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test \'abc\'']]), '{"breadcrumbs":' - . '{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test 'abc'"}}' + . '{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test 'abc'"}}' ], ]; } diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index 1903bcd144831..ea6b1fd47a0a5 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Ui\Component; +use Magento\Ui\Component\Filters\FilterModifier; + /** * Column Factory * @@ -60,13 +62,15 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ */ public function create($attribute, $context, array $config = []) { + $filterModifiers = $context->getRequestParam(FilterModifier::FILTER_MODIFIER, []); + $columnName = $attribute->getAttributeCode(); $config = array_merge([ 'label' => __($attribute->getDefaultFrontendLabel()), 'dataType' => $this->getDataType($attribute), 'add_field' => true, 'visible' => $attribute->getIsVisibleInGrid(), - 'filter' => ($attribute->getIsFilterableInGrid()) + 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) ? $this->getFilterType($attribute->getFrontendInput()) : null, ], $config); diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php index 5af0d71dc246c..494b77724e5b7 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -3,13 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Ui\Component\Listing\Columns; -use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\DB\Helper; use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; /** + * Websites listing column component. + * * @api * @since 100.0.2 */ @@ -20,6 +26,11 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column */ const NAME = 'websites'; + /** + * Data for concatenated website names value. + */ + private $websiteNames = 'website_names'; + /** * Store manager * @@ -27,26 +38,36 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column */ protected $storeManager; + /** + * @var \Magento\Framework\DB\Helper + */ + private $resourceHelper; + /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param StoreManagerInterface $storeManager * @param array $components * @param array $data + * @param Helper $resourceHelper */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, StoreManagerInterface $storeManager, array $components = [], - array $data = [] + array $data = [], + Helper $resourceHelper = null ) { parent::__construct($context, $uiComponentFactory, $components, $data); + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->storeManager = $storeManager; + $this->resourceHelper = $resourceHelper ?: $objectManager->get(Helper::class); } /** - * {@inheritdoc} + * @inheritdoc + * * @deprecated 101.0.0 */ public function prepareDataSource(array $dataSource) @@ -71,9 +92,10 @@ public function prepareDataSource(array $dataSource) return $dataSource; } - + /** - * Prepare component configuration + * Prepare component configuration. + * * @return void */ public function prepare() @@ -83,4 +105,46 @@ public function prepare() $this->_data['config']['componentDisabled'] = true; } } + + /** + * Apply sorting. + * + * @return void + */ + protected function applySorting() + { + $sorting = $this->getContext()->getRequestParam('sorting'); + $isSortable = $this->getData('config/sortable'); + if ($isSortable !== false + && !empty($sorting['field']) + && !empty($sorting['direction']) + && $sorting['field'] === $this->getName() + ) { + $collection = $this->getContext()->getDataProvider()->getCollection(); + $collection + ->joinField( + 'websites_ids', + 'catalog_product_website', + 'website_id', + 'product_id=entity_id', + null, + 'left' + ) + ->joinTable( + 'store_website', + 'website_id = websites_ids', + ['name'], + null, + 'left' + ) + ->groupByAttribute('entity_id'); + $this->resourceHelper->addGroupConcatColumn( + $collection->getSelect(), + $this->websiteNames, + 'name' + ); + + $collection->getSelect()->order($this->websiteNames . ' ' . $sorting['direction']); + } + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 336aeffa10584..00132c6ad89e8 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -139,7 +139,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -158,7 +159,8 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyData(array $data) @@ -381,11 +383,15 @@ private function addAdvancedPriceLink() ); $advancedPricingButton['arguments']['data']['config'] = [ + 'dataScope' => 'advanced_pricing_button', 'displayAsLink' => true, 'formElement' => Container::NAME, 'componentType' => Container::NAME, 'component' => 'Magento_Ui/js/form/components/button', 'template' => 'ui/form/components/button/container', + 'imports' => [ + 'childError' => $this->scopeName . '.advanced_pricing_modal.advanced-pricing:error', + ], 'actions' => [ [ 'targetName' => $this->scopeName . '.advanced_pricing_modal', diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 681435851fbde..800ead0e4030c 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Model\Locator\LocatorInterface; @@ -11,6 +13,7 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; @@ -202,6 +205,7 @@ protected function createNewCategoryModal(array $meta) * * @param array $meta * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function customizeCategoriesField(array $meta) @@ -306,20 +310,64 @@ protected function customizeCategoriesField(array $meta) * * @param string|null $filter * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function getCategoriesTree($filter = null) { - $categoryTree = $this->getCacheManager()->load(self::CATEGORY_TREE_ID . '_' . $filter); - if ($categoryTree) { - return $this->serializer->unserialize($categoryTree); + $storeId = (int) $this->locator->getStore()->getId(); + + $cachedCategoriesTree = $this->getCacheManager() + ->load($this->getCategoriesTreeCacheId($storeId, (string) $filter)); + if (!empty($cachedCategoriesTree)) { + return $this->serializer->unserialize($cachedCategoriesTree); } - $storeId = $this->locator->getStore()->getId(); + $categoriesTree = $this->retrieveCategoriesTree( + $storeId, + $this->retrieveShownCategoriesIds($storeId, (string) $filter) + ); + + $this->getCacheManager()->save( + $this->serializer->serialize($categoriesTree), + $this->getCategoriesTreeCacheId($storeId, (string) $filter), + [ + \Magento\Catalog\Model\Category::CACHE_TAG, + \Magento\Framework\App\Cache\Type\Block::CACHE_TAG + ] + ); + + return $categoriesTree; + } + + /** + * Get cache id for categories tree. + * + * @param int $storeId + * @param string $filter + * @return string + */ + private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + { + return self::CATEGORY_TREE_ID + . '_' . (string) $storeId + . '_' . $filter; + } + + /** + * Retrieve filtered list of categories id. + * + * @param int $storeId + * @param string $filter + * @return array + * @throws LocalizedException + */ + private function retrieveShownCategoriesIds(int $storeId, string $filter = '') : array + { /* @var $matchingNamesCollection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $matchingNamesCollection = $this->categoryCollectionFactory->create(); - if ($filter !== null) { + if (!empty($filter)) { $matchingNamesCollection->addAttributeToFilter( 'name', ['like' => $this->dbHelper->addLikeEscape($filter, ['position' => 'any'])] @@ -339,6 +387,19 @@ protected function getCategoriesTree($filter = null) } } + return $shownCategoriesIds; + } + + /** + * Retrieve tree of categories with attributes. + * + * @param int $storeId + * @param array $shownCategoriesIds + * @return array|null + * @throws LocalizedException + */ + private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds) : ?array + { /* @var $collection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $collection = $this->categoryCollectionFactory->create(); @@ -365,15 +426,6 @@ protected function getCategoriesTree($filter = null) $categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()]; } - $this->getCacheManager()->save( - $this->serializer->serialize($categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']), - self::CATEGORY_TREE_ID . '_' . $filter, - [ - \Magento\Catalog\Model\Category::CACHE_TAG, - \Magento\Framework\App\Cache\Type\Block::CACHE_TAG - ] - ); - return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']; } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php index 86f1db2022cc9..af43c84501f65 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Config\Source\Product\Options\Price as ProductOptionsPrice; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Ui\Component\Form\Element\Hidden; use Magento\Ui\Component\Modal; use Magento\Ui\Component\Container; use Magento\Ui\Component\DynamicRows; @@ -867,7 +868,7 @@ protected function getPositionFieldConfig($sortOrder) 'data' => [ 'config' => [ 'componentType' => Field::NAME, - 'formElement' => Input::NAME, + 'formElement' => Hidden::NAME, 'dataScope' => static::FIELD_SORT_ORDER_NAME, 'dataType' => Number::NAME, 'visible' => false, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php old mode 100755 new mode 100644 index 7379600011bcf..8326c3b531892 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -560,7 +560,7 @@ private function getAttributes() * Loads attributes for specified groups at once * * @param AttributeGroupInterface[] $groups - * @return @return ProductAttributeInterface[] + * @return ProductAttributeInterface[] */ private function loadAttributesForGroups(array $groups) { @@ -676,7 +676,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { - $options = $attributeModel->getSource()->getAllOptions(); + $options = $attributeModel->getSource()->getAllOptions(true, true); $meta = $this->arrayManager->merge($configPath, $meta, [ 'options' => $this->convertOptionsValueToString($options), ]); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php index 5513af9d98e7d..fed94193225f8 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php @@ -10,6 +10,9 @@ use Psr\Log\LoggerInterface as Logger; +/** + * Process config for Wysiwyg. + */ class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface { /** @@ -24,6 +27,7 @@ class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface /** * CompositeConfigProcessor constructor. + * @param Logger $logger * @param array $eavWysiwygDataProcessors */ public function __construct(Logger $logger, array $eavWysiwygDataProcessors) @@ -33,7 +37,7 @@ public function __construct(Logger $logger, array $eavWysiwygDataProcessors) } /** - * {@inheritdoc} + * @inheritdoc */ public function process(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index 6ec1cc6c46d9d..26044eb91a309 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -355,8 +355,10 @@ protected function customizeNameListeners(array $meta) 'allowImport' => !$this->locator->getProduct()->getId(), ]; - if (!in_array($listener, $textListeners)) { - $importsConfig['elementTmpl'] = 'ui/form/element/input'; + if (in_array($listener, $textListeners)) { + $importsConfig['cols'] = 15; + $importsConfig['rows'] = 2; + $importsConfig['elementTmpl'] = 'ui/form/element/textarea'; } $meta = $this->arrayManager->merge($listenerPath . static::META_CONFIG_PATH, $meta, $importsConfig); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php index 3090734df0144..4de0b94d06801 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php @@ -24,8 +24,6 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi /** * @param string $name - * @param string $primaryFieldName - * @param string $requestFieldName * @param Reporting $reporting * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param RequestInterface $request @@ -61,7 +59,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getData() { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php index f4334bc25efd8..e5451c8e49847 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -5,10 +5,16 @@ */ namespace Magento\Catalog\Ui\DataProvider\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Exception\LocalizedException; +use Magento\Eav\Model\Entity\Attribute\AttributeInterface; + /** * Collection which is used for rendering product list in the backend. * * Used for product grid and customizes behavior of the default Product collection for grid needs. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class ProductCollection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -25,4 +31,63 @@ protected function _productLimitationJoinPrice() $this->_productLimitationFilters->setUsePriceIndex(false); return $this->_productLimitationPrice(true); } + + /** + * Add attribute filter to collection + * + * @param AttributeInterface|integer|string|array $attribute + * @param null|string|array $condition + * @param string $joinType + * @return $this + * @throws LocalizedException + */ + public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') + { + $storeId = (int)$this->getStoreId(); + if ($attribute === 'is_saleable' + || is_array($attribute) + || $storeId !== $this->getDefaultStoreId() + ) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + if ($attribute instanceof AttributeInterface) { + $attributeModel = $attribute; + } else { + $attributeModel = $this->getEntity()->getAttribute($attribute); + if ($attributeModel === false) { + throw new LocalizedException( + __('Invalid attribute identifier for filter (%1)', get_class($attribute)) + ); + } + } + + if ($attributeModel->isScopeGlobal() || $attributeModel->getBackend()->isStatic()) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + $this->addAttributeToFilterAllStores($attributeModel, $condition); + + return $this; + } + + /** + * Add attribute to filter by all stores + * + * @param Attribute $attributeModel + * @param array $condition + * @return void + */ + private function addAttributeToFilterAllStores(Attribute $attributeModel, array $condition): void + { + $tableName = $this->getTable($attributeModel->getBackendTable()); + $entity = $this->getEntity(); + $fKey = 'e.' . $this->getEntityPkName($entity); + $pKey = $tableName . '.' . $this->getEntityPkName($entity); + $condition = "({$pKey} = {$fKey}) AND (" + . $this->_getConditionSql("{$tableName}.value", $condition) + . ')'; + $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); + $this->getSelect()->exists($selectExistsInAllStores, $condition); + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index 200ecf89641fa..a518afc576d61 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\Store; use Magento\Ui\DataProvider\Modifier\ModifierInterface; use Magento\Ui\DataProvider\Modifier\PoolInterface; @@ -67,6 +68,7 @@ public function __construct( $this->addFieldStrategies = $addFieldStrategies; $this->addFilterStrategies = $addFilterStrategies; $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class); + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); } /** @@ -110,7 +112,7 @@ public function addField($field, $alias = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function addFilter(\Magento\Framework\Api\Filter $filter) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php index 5d14cd21f7b95..3f16e0a6617da 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php @@ -21,7 +21,6 @@ interface ProductRenderCollectorInterface * * @param ProductInterface $product * @param ProductRenderInterface $productRender - * @param array $data * @return void * @since 101.1.0 */ diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php index 95f2531e5fdca..d1424d637937b 100644 --- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php +++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php @@ -105,7 +105,7 @@ public function getJsonConfigurationHtmlEscaped() : string [ 'breadcrumbs' => [ 'categoryUrlSuffix' => $this->escaper->escapeHtml($this->getCategoryUrlSuffix()), - 'userCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), + 'useCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), 'product' => $this->escaper->escapeHtml($this->getProductName()) ] ], diff --git a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php new file mode 100644 index 0000000000000..27829155af292 --- /dev/null +++ b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.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\ViewModel\Product\Checker; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; + +/** + * Check is available add to compare. + */ +class AddToCompareAvailability implements ArgumentInterface +{ + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct(StockConfigurationInterface $stockConfiguration) + { + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Is product available for comparison. + * + * @param ProductInterface $product + * @return bool + */ + public function isAvailableForCompare(ProductInterface $product): bool + { + return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock(); + } + + /** + * Get is in stock status. + * + * @param ProductInterface $product + * @return bool + */ + private function isInStock(ProductInterface $product): bool + { + $quantityAndStockStatus = $product->getQuantityAndStockStatus(); + if (!$quantityAndStockStatus) { + return $product->isSalable(); + } + + return isset($quantityAndStockStatus['is_in_stock']) && $quantityAndStockStatus['is_in_stock']; + } +} diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 44d051933909b..5c3ee3da8ca81 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -7,6 +7,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-asynchronous-operations": "*", "magento/module-backend": "*", "magento/module-catalog-inventory": "*", "magento/module-catalog-rule": "*", diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index fddc01ac4c189..c04cfb2dce00a 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -78,7 +78,7 @@ <type name="Magento\Catalog\Model\ResourceModel\Attribute"> <plugin name="invalidate_pagecache_after_attribute_save" type="Magento\Catalog\Plugin\Model\ResourceModel\Attribute\Save" /> </type> - <virtualType name="\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> + <virtualType name="Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> <arguments> <argument name="instanceName" xsi:type="string">\Magento\Catalog\Ui\DataProvider\Product\ProductCollection</argument> </arguments> diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 7a05601fcd666..1d563244f1432 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -59,7 +59,7 @@ <field id="grid_per_page_values" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Default Value</label> @@ -69,7 +69,7 @@ <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Default Value</label> diff --git a/app/code/Magento/Catalog/etc/communication.xml b/app/code/Magento/Catalog/etc/communication.xml new file mode 100644 index 0000000000000..1a957f6ac9fe5 --- /dev/null +++ b/app/code/Magento/Catalog/etc/communication.xml @@ -0,0 +1,15 @@ +<?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:Communication/etc/communication.xsd"> + <topic name="product_action_attribute.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="product_action_attribute.update" type="Magento\Catalog\Model\Attribute\Backend\Consumer" method="process" /> + </topic> + <topic name="product_action_attribute.website.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="product_action_attribute.website.update" type="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign" method="process" /> + </topic> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 7d2c3699ee2c2..49447447622f9 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -72,6 +72,7 @@ <preference for="Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface" type="Magento\Catalog\Model\Indexer\Product\Price\InvalidateIndex" /> <preference for="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactoryInterface" type="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactory" /> <preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" /> + <preference for="Magento\Catalog\Api\Data\MassActionInterface" type="\Magento\Catalog\Model\MassAction" /> <type name="Magento\Customer\Model\ResourceModel\Visitor"> <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> diff --git a/app/code/Magento/Catalog/etc/product_options.xsd b/app/code/Magento/Catalog/etc/product_options.xsd index 3bc24a9099262..734c8f378d5d7 100644 --- a/app/code/Magento/Catalog/etc/product_options.xsd +++ b/app/code/Magento/Catalog/etc/product_options.xsd @@ -61,11 +61,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/product_types_base.xsd b/app/code/Magento/Catalog/etc/product_types_base.xsd index 6cc35fd7bee37..dec952bcf492e 100644 --- a/app/code/Magento/Catalog/etc/product_types_base.xsd +++ b/app/code/Magento/Catalog/etc/product_types_base.xsd @@ -92,11 +92,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/queue.xml b/app/code/Magento/Catalog/etc/queue.xml new file mode 100644 index 0000000000000..137f34a5c1e25 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue.xml @@ -0,0 +1,15 @@ +<?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-message-queue:etc/queue.xsd"> + <broker topic="product_action_attribute.update" exchange="magento-db" type="db"> + <queue name="product_action_attribute.update" consumer="product_action_attribute.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process"/> + </broker> + <broker topic="product_action_attribute.website.update" exchange="magento-db" type="db"> + <queue name="product_action_attribute.website.update" consumer="product_action_attribute.website.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process"/> + </broker> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_consumer.xml b/app/code/Magento/Catalog/etc/queue_consumer.xml new file mode 100644 index 0000000000000..d9e66ae69c10c --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?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-message-queue:etc/consumer.xsd"> + <consumer name="product_action_attribute.update" queue="product_action_attribute.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process" /> + <consumer name="product_action_attribute.website.update" queue="product_action_attribute.website.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process" /> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_publisher.xml b/app/code/Magento/Catalog/etc/queue_publisher.xml new file mode 100644 index 0000000000000..1606ea42ec0b3 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_publisher.xml @@ -0,0 +1,15 @@ +<?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-message-queue:etc/publisher.xsd"> + <publisher topic="product_action_attribute.update"> + <connection name="db" exchange="magento-db" /> + </publisher> + <publisher topic="product_action_attribute.website.update"> + <connection name="db" exchange="magento-db" /> + </publisher> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_topology.xml b/app/code/Magento/Catalog/etc/queue_topology.xml new file mode 100644 index 0000000000000..bdac891afbdb8 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_topology.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-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="updateBinding" topic="product_action_attribute.update" destinationType="queue" destination="product_action_attribute.update"/> + <binding id="updateBindingWebsite" topic="product_action_attribute.website.update" destinationType="queue" destination="product_action_attribute.website.update"/> + </exchange> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 2a5d60222e9f8..44cdd473bf74e 100644 --- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml index 2a5d60222e9f8..44cdd473bf74e 100644 --- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml index 00a1580923a7b..ee67acd0ebd46 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml @@ -20,7 +20,7 @@ "categoryCheckboxTree": { "dataUrl": "<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>", "divId": "<?= /* @noEscape */ $divId ?>", - "rootVisible": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, + "rootVisible": false, "useAjax": <?= $block->escapeHtml($block->getUseAjax()) ?>, "currentNodeId": <?= (int)$block->getCategoryId() ?>, "jsFormObject": "<?= /* @noEscape */ $block->getJsFormObject() ?>", @@ -28,7 +28,7 @@ "checked": "<?= $block->escapeHtml($block->getRoot()->getChecked()) ?>", "allowdDrop": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, "rootId": <?= (int)$block->getRoot()->getId() ?>, - "expanded": <?= (int)$block->getIsWasExpanded() ?>, + "expanded": true, "categoryId": <?= (int)$block->getCategoryId() ?>, "treeJson": <?= /* @noEscape */ $block->getTreeJson() ?> } diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index 93666470b1b2c..f448edc692ce2 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -302,6 +302,7 @@ } <?php endif;?> //updateContent(url); //commented since ajax requests replaced with http ones to load a category + jQuery('#tree-div').find('.x-tree-node-el').first().remove(); } jQuery(function () { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml index dbe66ef1aecd3..69737b8a37c1c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -160,7 +160,7 @@ jQuery(function() loader: categoryLoader, enableDD: false, containerScroll: true, - rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', + rootVisible: false, useAjax: true, currentNodeId: <?= (int) $block->getCategoryId() ?>, addNodeTo: false @@ -177,7 +177,7 @@ jQuery(function() text: 'Psw', draggable: false, id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, + expanded: true, category_id: <?= (int) $block->getCategoryId() ?> }; diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index efc06d675c369..64c8ba7dcf49f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -30,6 +30,13 @@ }); </script> +<?php +$defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); +if (!is_numeric($defaultMinSaleQty)) { + $defaultMinSaleQty = json_decode($defaultMinSaleQty, true); + $defaultMinSaleQty = (float) $defaultMinSaleQty[\Magento\Customer\Api\Data\GroupInterface::CUST_GROUP_ALL] ?? 1; +} +?> <div class="fieldset-wrapper form-inline advanced-inventory-edit"> <div class="fieldset-wrapper-title"> <strong class="title"> @@ -132,7 +139,7 @@ <div class="field"> <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_sale_qty') * 1 ?>" + value="<?= /* @escapeNotVerified */ $defaultMinSaleQty ?>" disabled="disabled"/> </div> <div class="field choice"> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index 1a54db0d59f0f..90d6e0b48400e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -77,6 +77,13 @@ <dataType>text</dataType> </settings> </field> + <field name="level" formElement="hidden"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="source" xsi:type="string">category</item> + </item> + </argument> + </field> <field name="store_id" formElement="hidden"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index 65090fa3ac461..d689daef4bcab 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -132,6 +132,7 @@ <settings> <addField>true</addField> <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> <label translate="true">Name</label> </settings> </column> @@ -154,6 +155,7 @@ <column name="sku" sortOrder="60"> <settings> <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> <label translate="true">SKU</label> </settings> </column> @@ -190,6 +192,13 @@ <label translate="true">Websites</label> </settings> </column> + <column name="cost" class="Magento\Catalog\Ui\Component\Listing\Columns\Price" sortOrder="120"> + <settings> + <addField>true</addField> + <filter>textRange</filter> + <label translate="true">Cost</label> + </settings> + </column> <actionsColumn name="actions" class="Magento\Catalog\Ui\Component\Listing\Columns\ProductActions" sortOrder="200"> <settings> <indexField>entity_id</indexField> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js index 0a04358e41123..76aaddf55ac99 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js @@ -15,6 +15,7 @@ define([ categoryIdSelector: 'input[name="id"]', categoryPathSelector: 'input[name="path"]', categoryParentSelector: 'input[name="parent"]', + categoryLevelSelector: 'input[name="level"]', refreshUrl: config.refreshUrl }, @@ -47,6 +48,7 @@ define([ $(this.options.categoryIdSelector).val(data.id).change(); $(this.options.categoryPathSelector).val(data.path).change(); $(this.options.categoryParentSelector).val(data.parentId).change(); + $(this.options.categoryLevelSelector).val(data.level).change(); } } }; 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 a2804a8723ce0..1ac2a4ffadaae 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 @@ -91,8 +91,8 @@ define([ /** * Add product list types as scope and their urls - * expamle: addListType('product_to_add', {urlFetch: 'http://magento...'}) - * expamle: addListType('wishlist', {urlSubmit: 'http://magento...'}) + * example: addListType('product_to_add', {urlFetch: 'http://magento...'}) + * example: addListType('wishlist', {urlSubmit: 'http://magento...'}) * * @param type types as scope * @param urls obj can be @@ -112,7 +112,7 @@ define([ /** * Adds complex list type - that is used to submit several list types at once * Only urlSubmit is possible for this list type - * expamle: addComplexListType(['wishlist', 'product_list'], 'http://magento...') + * example: addComplexListType(['wishlist', 'product_list'], 'http://magento...') * * @param type types as scope * @param urls obj can be diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js index 407fd1fe28e39..e1923dc46d68e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js @@ -5,13 +5,13 @@ define([ 'jquery', - 'mage/mage' + 'mage/mage', + 'validation' ], function ($) { 'use strict'; return function (config, element) { - - $(element).mage('form').mage('validation', { + $(element).mage('form').validation({ validationUrl: config.validationUrl }); }; diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js index 97f978de47b60..f829c66c4011c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js @@ -5,10 +5,10 @@ define([ 'underscore', 'Magento_Ui/js/form/element/abstract' -], function (_, Acstract) { +], function (_, Abstract) { 'use strict'; - return Acstract.extend({ + return Abstract.extend({ defaults: { prefixName: '', prefixElementName: '', diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js index 2f6703cc92eac..4bbdea066b762 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js @@ -5,10 +5,10 @@ define([ 'underscore', 'Magento_Ui/js/form/element/abstract' -], function (_, Acstract) { +], function (_, Abstract) { 'use strict'; - return Acstract.extend({ + return Abstract.extend({ defaults: { prefixName: '', prefixElementName: '', diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml new file mode 100644 index 0000000000000..0f3b4f481a288 --- /dev/null +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +use Magento\Catalog\Model\Product\Option; + +/** + * @var \Magento\Catalog\Block\Product\View\Options\View\Checkable $block + */ +$option = $block->getOption(); +if ($option) : ?> + <?php + $configValue = $block->getPreconfiguredValue($option); + $optionType = $option->getType(); + $arraySign = $optionType === Option::OPTION_TYPE_CHECKBOX ? '[]' : ''; + $count = 1; + ?> + +<div class="options-list nested" id="options-<?php echo /* @noEscape */ +$option->getId() ?>-list"> + <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()): ?> + <div class="field choice admin__field admin__field-option"> + <input type="radio" + id="options_<?php echo /* @noEscape */ + $option->getId() ?>" + class="radio admin__control-radio product-custom-option" + name="options[<?php echo /* @noEscape */ + $option->getId() ?>]" + data-selector="options[<?php echo /* @noEscape */ + $option->getId() ?>]" + onclick="<?php echo $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + value="" + checked="checked" + /> + <label class="label admin__field-label" for="options_<?php echo /* @noEscape */ + $option->getId() ?>"> + <span> + <?php echo /* @noEscape */ + __('None') ?> + </span> + </label> + </div> +<?php endif; ?> + + <?php foreach ($option->getValues() as $value) : ?> + <?php + $checked = ''; + $count++; + if ($arraySign) { + $checked = is_array($configValue) && in_array($value->getOptionTypeId(), $configValue) ? 'checked' : ''; + } else { + $checked = $configValue == $value->getOptionTypeId() ? 'checked' : ''; + } + $dataSelector = 'options[' . $option->getId() . ']'; + if ($arraySign) { + $dataSelector .= '[' . $value->getOptionTypeId() . ']'; + } + ?> + + <div class="field choice admin__field admin__field-option <?php echo /* @noEscape */ + $option->getIsRequire() ? 'required': '' ?>"> + <input type="<?php echo /* @noEscape */ + $optionType ?>" + class="<?php /** @noinspection DisconnectedForeachInstructionInspection */ + echo /* @noEscape */ + $optionType === Option::OPTION_TYPE_RADIO ? + 'radio admin__control-radio' : + 'checkbox admin__control-checkbox' ?> <?php echo /* @noEscape */ + $option->getIsRequire() ? 'required': '' ?> + product-custom-option + <?php echo $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + name="options[<?php echo $option->getId() ?>]<?php echo /* @noEscape */ + $arraySign ?>" + id="options_<?php echo /* @noEscape */ + $option->getId() . '_' . $count ?>" + value="<?php echo /* @noEscape */ + $value->getOptionTypeId() ?>" + <?php echo /* @noEscape */ + $checked ?> + data-selector="<?php echo /* @noEscape */ + $dataSelector ?>" + price="<?php echo /* @noEscape */ + $block->getCurrencyByStore($value) ?>" + /> + <label class="label admin__field-label" + for="options_<?php echo /* @noEscape */ + $option->getId() . '_' . $count ?>"> + <span> + <?php echo $block->escapeHtml($value->getTitle()) ?> + </span> + <?php echo /* @noEscape */ + $block->formatPrice($value) ?> + </label> + </div> + <?php endforeach; ?> + </div> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/base/web/js/price-box.js b/app/code/Magento/Catalog/view/base/web/js/price-box.js index de68d769885fd..783d39cddbc76 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-box.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-box.js @@ -78,11 +78,7 @@ define([ pricesCode = [], priceValue, origin, finalPrice; - if (typeof newPrices !== 'undefined' && newPrices.hasOwnProperty('prices')) { - this.cache.additionalPriceObject = {}; - } else { - this.cache.additionalPriceObject = this.cache.additionalPriceObject || {}; - } + this.cache.additionalPriceObject = this.cache.additionalPriceObject || {}; if (newPrices) { $.extend(this.cache.additionalPriceObject, newPrices); diff --git a/app/code/Magento/Catalog/view/base/web/js/price-utils.js b/app/code/Magento/Catalog/view/base/web/js/price-utils.js index e2ea42f7d5fe3..7b83d12cc9804 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-utils.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-utils.js @@ -60,7 +60,7 @@ define([ pattern = pattern.indexOf('{sign}') < 0 ? s + pattern : pattern.replace('{sign}', s); // we're avoiding the usage of to fixed, and using round instead with the e representation to address - // numbers like 1.005 = 1.01. Using ToFixed to only provide trailig zeroes in case we have a whole number + // numbers like 1.005 = 1.01. Using ToFixed to only provide trailing zeroes in case we have a whole number i = parseInt( amount = Number(Math.round(Math.abs(+amount || 0) + 'e+' + precision) + ('e-' + precision)), 10 diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml index 3630fddb326a7..13e2d998f6cdd 100644 --- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml @@ -91,7 +91,11 @@ <container name="product.info.social" label="Product social links container" htmlTag="div" htmlClass="product-social-links"> <block class="Magento\Catalog\Block\Product\View" name="product.info.addto" as="addto" template="Magento_Catalog::product/view/addto.phtml"> <block class="Magento\Catalog\Block\Product\View\AddTo\Compare" name="view.addto.compare" after="view.addto.wishlist" - template="Magento_Catalog::product/view/addto/compare.phtml" /> + template="Magento_Catalog::product/view/addto/compare.phtml" > + <arguments> + <argument name="addToCompareViewModel" xsi:type="object">Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability</argument> + </arguments> + </block> </block> <block class="Magento\Catalog\Block\Product\View" name="product.info.mailto" template="Magento_Catalog::product/view/mailto.phtml"/> </container> @@ -121,7 +125,12 @@ </arguments> </block> </container> - <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"/> + <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"> + <arguments> + <argument name="gallery_options" xsi:type="object">Magento\Catalog\Block\Product\View\GalleryOptions</argument> + <argument name="imageHelper" xsi:type="object">Magento\Catalog\Helper\Image</argument> + </arguments> + </block> <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml"> <arguments> @@ -136,7 +145,7 @@ </arguments> </block> </container> - <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> + <block class="Magento\Catalog\Block\Product\View\Details" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" as="description" template="Magento_Catalog::product/view/attribute.phtml" group="detailed_info"> <arguments> <argument name="at_call" xsi:type="string">getDescription</argument> @@ -144,11 +153,13 @@ <argument name="css_class" xsi:type="string">description</argument> <argument name="at_label" xsi:type="string">none</argument> <argument name="title" translate="true" xsi:type="string">Details</argument> + <argument name="sort_order" xsi:type="string">10</argument> </arguments> </block> <block class="Magento\Catalog\Block\Product\View\Attributes" name="product.attributes" as="additional" template="Magento_Catalog::product/view/attributes.phtml" group="detailed_info"> <arguments> <argument translate="true" name="title" xsi:type="string">More Information</argument> + <argument name="sort_order" xsi:type="string">20</argument> </arguments> </block> </block> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 949d365e7899a..7daf049980362 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -116,7 +116,9 @@ <?php $block->getImage($item, 'product_small_image')->toHtml(); ?> <?php break; default: ?> - <?= /* @escapeNotVerified */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> + <?php if (is_string($block->getProductAttributeValue($item, $attribute))): ?> + <?= /* @escapeNotVerified */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> + <?php endif; ?> <?php break; } ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index f434402346087..ecc9700802d27 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -169,7 +169,7 @@ switch ($type = $block->getType()) { <?php if ($type == 'related' && $canItemsAddToCart): ?> <div class="block-actions"> <?= /* @escapeNotVerified */ __('Check items to add to the cart or') ?> - <button type="button" class="action select" role="select-all"><span><?= /* @escapeNotVerified */ __('select all') ?></span></button> + <button type="button" class="action select" role="button"><span><?= /* @escapeNotVerified */ __('select all') ?></span></button> </div> <?php endif; ?> <div class="products wrapper grid products-grid products-<?= /* @escapeNotVerified */ $type ?>"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml index adf0f44d0c831..194a472d81d58 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml @@ -9,6 +9,10 @@ /** @var $block \Magento\Catalog\Block\Product\View\Addto\Compare */ ?> +<?php $viewModel = $block->getData('addToCompareViewModel'); ?> +<?php if ($viewModel->isAvailableForCompare($block->getProduct())): ?> <a href="#" data-post='<?= /* @escapeNotVerified */ $block->getPostDataParams() ?>' data-role="add-to-links" class="action tocompare"><span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span></a> +<?php endif; ?> + diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml index 9c18a18ff5837..71452a2d65e97 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml @@ -20,6 +20,7 @@ <input type="number" name="qty" id="qty" + min="0" value="<?= /* @escapeNotVerified */ $block->getProductDefaultQty() * 1 ?>" title="<?= /* @escapeNotVerified */ __('Qty') ?>" class="input-text qty" @@ -32,7 +33,7 @@ <button type="submit" title="<?= /* @escapeNotVerified */ $buttonTitle ?>" class="action primary tocart" - id="product-addtocart-button"> + id="product-addtocart-button" disabled> <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> </button> <?= $block->getChildHtml('', true) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml index c930d2195a01b..1c4a37fedebe3 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml @@ -23,8 +23,8 @@ <tbody> <?php foreach ($_additional as $_data): ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__($_data['label'])) ?></th> - <td class="col data" data-th="<?= $block->escapeHtml(__($_data['label'])) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> + <th class="col label" scope="row"><?= $block->escapeHtml($_data['label']) ?></th> + <td class="col data" data-th="<?= $block->escapeHtml($_data['label']) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> </tr> <?php endforeach; ?> </tbody> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml index 038bea86e7d4e..57eabbf1d8c8a 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml @@ -6,8 +6,9 @@ // @codingStandardsIgnoreFile +/** @var \Magento\Catalog\Block\Product\View\Details $block */ ?> -<?php if ($detailedInfoGroup = $block->getGroupChildNames('detailed_info', 'getChildHtml')):?> +<?php if ($detailedInfoGroup = $block->getGroupSortedChildNames('detailed_info', 'getChildHtml')):?> <div class="product info detailed"> <?php $layout = $block->getLayout(); ?> <div class="product data items" data-mage-init='{"tabs":{"openedState":"active"}}'> @@ -21,17 +22,17 @@ $label = $block->getChildData($alias, 'title'); ?> <div class="data item title" - aria-labelledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" data-role="collapsible" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>"> <a class="data switch" tabindex="-1" - data-toggle="switch" + data-toggle="trigger" href="#<?= /* @escapeNotVerified */ $alias ?>" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title"> <?= /* @escapeNotVerified */ $label ?> </a> </div> - <div class="data item content" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> + <div class="data item content" + aria-labelledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> <?= /* @escapeNotVerified */ $html ?> </div> <?php endforeach;?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml index 1bfa30478df8a..1f06b90758d0b 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml @@ -12,32 +12,32 @@ * @var $block \Magento\Catalog\Block\Product\View\Gallery */ ?> -<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder"> - <div data-role="loader" class="loading-mask"> - <div class="loader"> - <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" - alt="<?= /* @escapeNotVerified */ __('Loading...') ?>"> - </div> - </div> -</div> -<!--Fix for jumping content. Loader must be the same size as gallery.--> -<script> - var config = { - "width": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>, - "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", - "height": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'height') ?> - }, - thumbBarHeight = 0, - loader = document.querySelectorAll('[data-gallery-role="gallery-placeholder"] [data-role="loader"]')[0]; - if (config.navtype === 'horizontal') { - thumbBarHeight = config.thumbheight; +<?php + $images = $block->getGalleryImages()->getItems(); + $mainImage = current(array_filter($images, function ($img) use ($block) { + return $block->isMainImage($img); + })); + + if (!empty($images) && empty($mainImage)) { + $mainImage = $block->getGalleryImages()->getFirstItem(); } - loader.style.paddingBottom = ( config.height / config.width * 100) + "%"; -</script> + $helper = $block->getData('imageHelper'); + $mainImageData = $mainImage ? + $mainImage->getData('medium_image_url') : + $helper->getDefaultPlaceholderUrl('image'); + +?> + +<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder"> + <img + alt="main product photo" + class="gallery-placeholder__image" + src="<?= /* @noEscape */ $mainImageData ?>" + /> +</div> + <script type="text/x-magento-init"> { "[data-gallery-role=gallery-placeholder]": { @@ -45,44 +45,8 @@ "mixins":["magnifier/magnify"], "magnifierOpts": <?= /* @escapeNotVerified */ $block->getMagnifier() ?>, "data": <?= /* @escapeNotVerified */ $block->getGalleryImagesJson() ?>, - "options": { - "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/nav") ?>", - "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/loop") ? 'true' : 'false' ?>, - "keyboard": <?= /* @escapeNotVerified */ $block->getVar("gallery/keyboard") ? 'true' : 'false' ?>, - "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/arrows") ? 'true' : 'false' ?>, - "allowfullscreen": <?= /* @escapeNotVerified */ $block->getVar("gallery/allowfullscreen") ? 'true' : 'false' ?>, - "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/caption") ? 'true' : 'false' ?>, - "width": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>", - "thumbwidth": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_small', 'width') ?>", - <?php if ($block->getImageAttribute('product_page_image_small', 'height') || $block->getImageAttribute('product_page_image_small', 'width')): ?> - "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, - <?php endif; ?> - <?php if ($block->getImageAttribute('product_page_image_medium', 'height') || $block->getImageAttribute('product_page_image_medium', 'width')): ?> - "height": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_medium', 'height') - ?: $block->getImageAttribute('product_page_image_medium', 'width'); ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/transition/duration")): ?> - "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/transition/duration") ?>, - <?php endif; ?> - "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/transition/effect") ?>", - "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/navarrows") ? 'true' : 'false' ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", - "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navdir") ?>" - }, - "fullscreen": { - "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/nav") ?>", - "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/loop") ? 'true' : 'false' ?>, - "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navdir") ?>", - "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navarrows") ? 'true' : 'false' ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navtype") ?>", - "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/arrows") ? 'true' : 'false' ?>, - "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/caption") ? 'true' : 'false' ?>, - <?php if ($block->getVar("gallery/fullscreen/transition/duration")): ?> - "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/duration") ?>, - <?php endif; ?> - "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/effect") ?>" - }, + "options": <?= /* @noEscape */ $block->getGalleryOptions()->getOptionsJson() ?>, + "fullscreen": <?= /* @noEscape */ $block->getGalleryOptions()->getFSOptionsJson() ?>, "breakpoints": <?= /* @escapeNotVerified */ $block->getBreakpoints() ?> } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml index a2b91a5eeb99f..40f86c7e68d6c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml @@ -14,7 +14,7 @@ <meta property="og:image" content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> <meta property="og:description" content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getShortDescription())) ?>" /> <meta property="og:url" content="<?= $block->escapeUrl($block->getProduct()->getProductUrl()) ?>" /> -<?php if ($priceAmount = $block->getProduct()->getFinalPrice()):?> +<?php if ($priceAmount = $block->getProduct()->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)->getAmount()):?> <meta property="product:price:amount" content="<?= /* @escapeNotVerified */ $priceAmount ?>"/> <?= $block->getChildHtml('meta.currency') ?> <?php endif;?> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index 7434678d1694b..bcb7c668657d3 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -165,6 +165,16 @@ define([ self.enableAddToCartButton(form); }, + /** @inheritdoc */ + error: function (res) { + $(document).trigger('ajax:addToCart:error', { + 'sku': form.data().productSku, + 'productIds': productIds, + 'form': form, + 'response': res + }); + }, + /** @inheritdoc */ complete: function (res) { if (res.state() === 'rejected') { diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js index 0c37f9ff4f007..66df48c28bfab 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js @@ -17,7 +17,7 @@ define([ relatedProductsField: '#related-products-field', // Hidden input field that stores related products. selectAllMessage: $.mage.__('select all'), unselectAllMessage: $.mage.__('unselect all'), - selectAllLink: '[role="select-all"]', + selectAllLink: '[role="button"]', elementsSelector: '.item.product' }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js index c0637cb672dc6..755e777a01f77 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js @@ -13,7 +13,8 @@ define([ $.widget('mage.productValidate', { options: { bindSubmit: false, - radioCheckboxClosest: '.nested' + radioCheckboxClosest: '.nested', + addToCartButtonSelector: '.action.tocart' }, /** @@ -41,6 +42,7 @@ define([ return false; } }); + $(this.options.addToCartButtonSelector).attr('disabled', false); } }); diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json index 5c97261d483d8..805be8a17765f 100644 --- a/app/code/Magento/CatalogAnalytics/composer.json +++ b/app/code/Magento/CatalogAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-catalog": "*" + "magento/module-catalog": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index dbe58a9c77cd0..b5d02511da4e7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -26,7 +26,7 @@ public function calculate(FieldNode $fieldNode) : int $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { - if ($node->kind === 'InlineFragment') { + if ($node->kind === 'InlineFragment' || null !== $node->alias) { continue; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php index 0401e1c42331e..f587be245c99d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php @@ -15,6 +15,16 @@ */ class LevelCalculator { + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var Category + */ + private $resourceCategory; + /** * @param ResourceConnection $resourceConnection * @param Category $resourceCategory @@ -39,6 +49,7 @@ public function calculate(int $rootCategoryId) : int $select = $connection->select() ->from($this->resourceConnection->getTableName('catalog_category_entity'), 'level') ->where($this->resourceCategory->getLinkField() . " = ?", $rootCategoryId); + return (int) $connection->fetchOne($select); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php index 4e3a8403f3132..1783a5cd9a7e5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -72,11 +73,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $rootCategoryId = $this->getCategoryId($args); $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId); - if (!empty($categoriesTree)) { - $result = $this->extractDataFromCategoryTree->execute($categoriesTree); - return current($result); - } else { - return null; + + if (empty($categoriesTree) || ($categoriesTree->count() == 0)) { + throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist')); } + + $result = $this->extractDataFromCategoryTree->execute($categoriesTree); + return current($result); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php index 3c19ce599a9b3..23a8c2d15c09e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\ImageFactory; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image\Placeholder as PlaceholderProvider; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -23,14 +24,21 @@ class Url implements ResolverInterface * @var ImageFactory */ private $productImageFactory; + /** + * @var PlaceholderProvider + */ + private $placeholderProvider; /** * @param ImageFactory $productImageFactory + * @param PlaceholderProvider $placeholderProvider */ public function __construct( - ImageFactory $productImageFactory + ImageFactory $productImageFactory, + PlaceholderProvider $placeholderProvider ) { $this->productImageFactory = $productImageFactory; + $this->placeholderProvider = $placeholderProvider; } /** @@ -55,23 +63,27 @@ public function resolve( $product = $value['model']; $imagePath = $product->getData($value['image_type']); - $imageUrl = $this->getImageUrl($value['image_type'], $imagePath); - return $imageUrl; + return $this->getImageUrl($value['image_type'], $imagePath); } /** - * Get image url + * Get image URL * * @param string $imageType - * @param string|null $imagePath Null if image is not set + * @param string|null $imagePath * @return string + * @throws \Exception */ private function getImageUrl(string $imageType, ?string $imagePath): string { $image = $this->productImageFactory->create(); $image->setDestinationSubdir($imageType) ->setBaseFile($imagePath); - $imageUrl = $image->getUrl(); - return $imageUrl; + + if ($image->isBaseFilePlaceholder()) { + return $this->placeholderProvider->getPlaceholder($imageType); + } + + return $image->getUrl(); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index ff53299b00e33..24c5e664831e4 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -7,7 +7,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver; -use Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Filter; use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index f2634574a2d15..fc5a563c82b4e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -101,11 +101,21 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato $collection->addFieldToFilter('level', ['gt' => $level]); $collection->addFieldToFilter('level', ['lteq' => $level + $depth - self::DEPTH_OFFSET]); + $collection->addAttributeToFilter('is_active', 1, "left"); $collection->setOrder('level'); + $collection->setOrder( + 'position', + $collection::SORT_ORDER_DESC + ); $collection->getSelect()->orWhere( - $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() . ' = ?', + $collection->getSelect() + ->getConnection() + ->quoteIdentifier( + 'e.' . $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() + ) . ' = ?', $rootCategoryId ); + return $collection->getIterator(); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php index ac8d5709c85b3..3525ccbb6a2d1 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php @@ -20,6 +20,16 @@ class ExtractDataFromCategoryTree */ private $categoryHydrator; + /** + * @var CategoryInterface + */ + private $iteratingCategory; + + /** + * @var int + */ + private $startCategoryFetchLevel = 1; + /** * @param Hydrator $categoryHydrator */ @@ -42,14 +52,60 @@ public function execute(\Iterator $iterator): array /** @var CategoryInterface $category */ $category = $iterator->current(); $iterator->next(); - $nextCategory = $iterator->current(); - $tree[$category->getId()] = $this->categoryHydrator->hydrateCategory($category); - $tree[$category->getId()]['model'] = $category; - if ($nextCategory && (int) $nextCategory->getLevel() !== (int) $category->getLevel()) { - $tree[$category->getId()]['children'] = $this->execute($iterator); + $pathElements = explode("/", $category->getPath()); + if (empty($tree)) { + $this->startCategoryFetchLevel = count($pathElements) - 1; + } + $this->iteratingCategory = $category; + $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel); + if (empty($tree)) { + $tree = $currentLevelTree; + } + $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree); + } + return $tree; + } + + /** + * Merge together complex categories trees + * + * @param array $tree1 + * @param array $tree2 + * @return array + */ + private function mergeCategoriesTrees(array &$tree1, array &$tree2): array + { + $mergedTree = $tree1; + foreach ($tree2 as $currentKey => &$value) { + if (is_array($value) && isset($mergedTree[$currentKey]) && is_array($mergedTree[$currentKey])) { + $mergedTree[$currentKey] = $this->mergeCategoriesTrees($mergedTree[$currentKey], $value); + } else { + $mergedTree[$currentKey] = $value; } } + return $mergedTree; + } + /** + * Recursive method to generate tree for one category path + * + * @param array $pathElements + * @param int $index + * @return array + */ + private function explodePathToArray(array $pathElements, int $index): array + { + $tree = []; + $tree[$pathElements[$index]]['id'] = $pathElements[$index]; + if ($index === count($pathElements) - 1) { + $tree[$pathElements[$index]] = $this->categoryHydrator->hydrateCategory($this->iteratingCategory); + $tree[$pathElements[$index]]['model'] = $this->iteratingCategory; + } + $currentIndex = $index; + $index++; + if (isset($pathElements[$index])) { + $tree[$pathElements[$currentIndex]]['children'] = $this->explodePathToArray($pathElements, $index); + } return $tree; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php new file mode 100644 index 0000000000000..f5cf2a9ef82ff --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image; + +use Magento\Catalog\Model\View\Asset\PlaceholderFactory; +use Magento\Framework\View\Asset\Repository as AssetRepository; + +/** + * Image Placeholder provider + */ +class Placeholder +{ + /** + * @var PlaceholderFactory + */ + private $placeholderFactory; + + /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @param PlaceholderFactory $placeholderFactory + * @param AssetRepository $assetRepository + */ + public function __construct( + PlaceholderFactory $placeholderFactory, + AssetRepository $assetRepository + ) { + $this->placeholderFactory = $placeholderFactory; + $this->assetRepository = $assetRepository; + } + + /** + * Get placeholder + * + * @param string $imageType + * @return string + */ + public function getPlaceholder(string $imageType): string + { + $imageAsset = $this->placeholderFactory->create(['type' => $imageType]); + + // check if placeholder defined in config + if ($imageAsset->getFilePath()) { + return $imageAsset->getUrl(); + } + + return $this->assetRepository->getUrl( + "Magento_Catalog::images/product/placeholder/{$imageType}.jpg" + ); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php new file mode 100644 index 0000000000000..dc48c5ef69346 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image\Placeholder; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Theme provider + */ +class Theme +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param ThemeProviderInterface $themeProvider + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + ThemeProviderInterface $themeProvider + ) { + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; + $this->themeProvider = $themeProvider; + } + + /** + * Get theme model + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getThemeData(): array + { + $themeId = $this->scopeConfig->getValue( + \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore()->getId() + ); + + /** @var $theme \Magento\Framework\View\Design\ThemeInterface */ + $theme = $this->themeProvider->getThemeById($themeId); + + $data = $theme->getData(); + $data['themeModel'] = $theme; + + return $data; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php b/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php new file mode 100644 index 0000000000000..4490cf031e5e1 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Search\Adapter\Mysql\Query\Builder; + +use Magento\Framework\DB\Helper\Mysql\Fulltext; +use Magento\Framework\Search\Adapter\Mysql\Field\ResolverInterface; +use Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match as BuilderMatch; +use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; +use Magento\Framework\Search\Request\Query\BoolExpression; +use Magento\Search\Helper\Data; + +/** + * @inheritdoc + */ +class Match extends BuilderMatch +{ + /** + * @var Data + */ + private $searchHelper; + + /** + * @param ResolverInterface $resolver + * @param Fulltext $fulltextHelper + * @param Data $searchHelper + * @param string $fulltextSearchMode + * @param PreprocessorInterface[] $preprocessors + */ + public function __construct( + ResolverInterface $resolver, + Fulltext $fulltextHelper, + Data $searchHelper, + $fulltextSearchMode = Fulltext::FULLTEXT_MODE_BOOLEAN, + array $preprocessors = [] + ) { + parent::__construct($resolver, $fulltextHelper, $fulltextSearchMode, $preprocessors); + $this->searchHelper = $searchHelper; + } + + /** + * @inheritdoc + */ + protected function prepareQuery($queryValue, $conditionType) + { + $replaceSymbols = str_split(self::SPECIAL_CHARACTERS, 1); + $queryValue = str_replace($replaceSymbols, ' ', $queryValue); + foreach ($this->preprocessors as $preprocessor) { + $queryValue = $preprocessor->process($queryValue); + } + + $stringPrefix = ''; + if ($conditionType === BoolExpression::QUERY_CONDITION_MUST) { + $stringPrefix = '+'; + } elseif ($conditionType === BoolExpression::QUERY_CONDITION_NOT) { + $stringPrefix = '-'; + } + + $queryValues = explode(' ', $queryValue); + + foreach ($queryValues as $queryKey => $queryValue) { + if (empty($queryValue)) { + unset($queryValues[$queryKey]); + } else { + $stringSuffix = $this->searchHelper->getMinQueryLength() > strlen($queryValue) ? '' : '*'; + $queryValues[$queryKey] = $stringPrefix . $queryValue . $stringSuffix; + } + } + + $queryValue = implode(' ', $queryValues); + + return $queryValue; + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 7e18ac34f0fcc..a5bd42860ded0 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -38,11 +38,15 @@ </item> <item name="customizable_options" xsi:type="array"> <item name="field" xsi:type="string">CustomizableFieldOption</item> + <item name="date" xsi:type="string">CustomizableDateOption</item> <item name="date_time" xsi:type="string">CustomizableDateOption</item> + <item name="time" xsi:type="string">CustomizableDateOption</item> <item name="file" xsi:type="string">CustomizableFileOption</item> <item name="area" xsi:type="string">CustomizableAreaOption</item> <item name="drop_down" xsi:type="string">CustomizableDropDownOption</item> + <item name="multiple" xsi:type="string">CustomizableMultipleOption</item> <item name="radio" xsi:type="string">CustomizableRadioOption</item> + <item name="checkbox" xsi:type="string">CustomizableCheckboxOption</item> </item> </argument> </arguments> @@ -74,4 +78,6 @@ </argument> </arguments> </virtualType> + <preference for="Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match" + type="Magento\CatalogGraphQl\Model\Search\Adapter\Mysql\Query\Builder\Match" /> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 45f3a4c83be7b..dc37b3ce76113 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -323,6 +323,19 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sort_order: Int @doc(description: "The order in which the option is displayed") } +type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option") { + value: [CustomizableMultipleValue] @doc(description: "An array that defines the set of options for a multiselect") +} + +type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defines the price and sku of a product whose page contains a customized multiselect") { + option_type_id: Int @doc(description: "The ID assigned to the value") + 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") + title: String @doc(description: "The display name for this option") + sort_order: Int @doc(description: "The order in which the option is displayed") +} + type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option") { value: CustomizableFieldValue @doc(description: "An object that defines a text field") product_sku: String @doc(description: "The Stock Keeping Unit of the base product") @@ -407,6 +420,19 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines t sort_order: Int @doc(description: "The order in which the radio button is displayed") } +type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option") { + value: [CustomizableCheckboxValue] @doc(description: "An array that defines a set of checkbox values") +} + +type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defines the price and sku of a product whose page contains a customized set of checkbox values") { + option_type_id: Int @doc(description: "The ID assigned to the value") + 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") + title: String @doc(description: "The display name for this option") + sort_order: Int @doc(description: "The order in which the checkbox value is displayed") +} + 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") { } @@ -434,7 +460,7 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") short_description: FilterTypeInput @doc(description: "A short description of the product. Its use depends on the theme.") price: FilterTypeInput @doc(description: "The price of an item") - special_price: FilterTypeInput @doc(description: "The discounted price of the product") + special_price: FilterTypeInput @doc(description: "The discounted price of the product. Do not include the currency code.") special_from_date: FilterTypeInput @doc(description: "The beginning date that a product has a special price") special_to_date: FilterTypeInput @doc(description: "The end date that a product has a special price") weight: FilterTypeInput @doc(description: "The weight of the item, in units defined by the store") @@ -451,7 +477,6 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte custom_layout_update: FilterTypeInput @doc(description: "XML code that is applied as a layout update to the product page") min_price: FilterTypeInput @doc(description:"The numeric minimal price of the product. Do not include the currency code.") max_price: FilterTypeInput @doc(description:"The numeric maximal price of the product. Do not include the currency code.") - special_price: FilterTypeInput @doc(description:"The numeric special price of the product. Do not include the currency code.") category_id: FilterTypeInput @doc(description: "Category ID the product belongs to") options_container: FilterTypeInput @doc(description: "If the product has multiple options, determines where they appear on the product page") required_options: FilterTypeInput @doc(description: "Indicates whether the product has required options") diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index c047e9c9bef21..75249e4907862 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -351,6 +351,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity /** * Product constructor. + * * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Eav\Model\Config $config * @param \Magento\Framework\App\ResourceConnection $resource @@ -941,15 +942,17 @@ protected function getExportData() protected function loadCollection(): array { $data = []; - $collection = $this->_getEntityCollection(); foreach (array_keys($this->_storeIdToCode) as $storeId) { + $collection->setOrder('entity_id', 'asc'); + $this->_prepareEntityCollection($collection); $collection->setStoreId($storeId); + $collection->load(); foreach ($collection as $itemId => $item) { $data[$itemId][$storeId] = $item; } + $collection->clear(); } - $collection->clear(); return $data; } @@ -1294,11 +1297,23 @@ private function appendMultirowData(&$dataRow, $multiRawData) } if (!empty($multiRawData['customOptionsData'][$productLinkId][$storeId])) { + $shouldBeMerged = true; $customOptionsRows = $multiRawData['customOptionsData'][$productLinkId][$storeId]; - $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; - $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); - $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); + if ($storeId != Store::DEFAULT_STORE_ID + && !empty($multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]) + ) { + $defaultCustomOptions = $multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]; + if (!array_diff($defaultCustomOptions, $customOptionsRows)) { + $shouldBeMerged = false; + } + } + + if ($shouldBeMerged) { + $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; + $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); + $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); + } } if (empty($dataRow)) { @@ -1394,6 +1409,7 @@ protected function optionRowToCellString($option) protected function getCustomOptionsData($productIds) { $customOptionsData = []; + $defaultOptionsData = []; foreach (array_keys($this->_storeIdToCode) as $storeId) { $options = $this->_optionColFactory->create(); @@ -1406,38 +1422,42 @@ protected function getCustomOptionsData($productIds) ->addValuesToResult($storeId); foreach ($options as $option) { + $optionData = $option->toArray(); $row = []; $productId = $option['product_id']; $row['name'] = $option['title']; $row['type'] = $option['type']; - if (Store::DEFAULT_STORE_ID === $storeId) { - $row['required'] = $option['is_require']; - $row['price'] = $option['price']; - $row['price_type'] = ($option['price_type'] === 'percent') ? 'percent' : 'fixed'; - $row['sku'] = $option['sku']; - if ($option['max_characters']) { - $row['max_characters'] = $option['max_characters']; - } - - foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { - if (!isset($option[$fileOptionKey])) { - continue; - } - $row[$fileOptionKey] = $option[$fileOptionKey]; + $row['required'] = $this->getOptionValue('is_require', $defaultOptionsData, $optionData); + $row['price'] = $this->getOptionValue('price', $defaultOptionsData, $optionData); + $row['sku'] = $this->getOptionValue('sku', $defaultOptionsData, $optionData); + if (array_key_exists('max_characters', $optionData) + || array_key_exists('max_characters', $defaultOptionsData) + ) { + $row['max_characters'] = $this->getOptionValue('max_characters', $defaultOptionsData, $optionData); + } + foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { + if (isset($option[$fileOptionKey]) || isset($defaultOptionsData[$fileOptionKey])) { + $row[$fileOptionKey] = $this->getOptionValue($fileOptionKey, $defaultOptionsData, $optionData); } } + $percentType = $this->getOptionValue('price_type', $defaultOptionsData, $optionData); + $row['price_type'] = ($percentType === 'percent') ? 'percent' : 'fixed'; + + if (Store::DEFAULT_STORE_ID === $storeId) { + $optionId = $option['option_id']; + $defaultOptionsData[$optionId] = $option->toArray(); + } + $values = $option->getValues(); if ($values) { foreach ($values as $value) { $row['option_title'] = $value['title']; - if (Store::DEFAULT_STORE_ID === $storeId) { - $row['option_title'] = $value['title']; - $row['price'] = $value['price']; - $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; - $row['sku'] = $value['sku']; - } + $row['option_title'] = $value['title']; + $row['price'] = $value['price']; + $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; + $row['sku'] = $value['sku']; $customOptionsData[$productId][$storeId][] = $this->optionRowToCellString($row); } } else { @@ -1451,6 +1471,31 @@ protected function getCustomOptionsData($productIds) return $customOptionsData; } + /** + * Get value for custom option according to store or default value + * + * @param string $optionName + * @param array $defaultOptionsData + * @param array $optionData + * @return mixed + */ + private function getOptionValue($optionName, $defaultOptionsData, $optionData) + { + $optionId = $optionData['option_id']; + + if (array_key_exists($optionName, $optionData) && $optionData[$optionName] !== null) { + return $optionData[$optionName]; + } + + if (array_key_exists($optionId, $defaultOptionsData) + && array_key_exists($optionName, $defaultOptionsData[$optionId]) + ) { + return $defaultOptionsData[$optionId][$optionName]; + } + + return null; + } + /** * Clean up already loaded attribute collection. * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 9298939791e4b..edeb955b19c9b 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -3,16 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Model\Import; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; -use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; +use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; -use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogImportExport\Model\StockItemImporterInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; @@ -132,16 +133,6 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ const COL_NAME = 'name'; - /** - * Column new_from_date. - */ - const COL_NEW_FROM_DATE = 'new_from_date'; - - /** - * Column new_to_date. - */ - const COL_NEW_TO_DATE = 'new_to_date'; - /** * Column product website. */ @@ -307,8 +298,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', - ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => "Value for multiselect attribute %s contains duplicated values", - ValidatorInterface::ERROR_NEW_TO_DATE => 'Make sure new_to_date is later than or the same as new_from_date', + ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', + 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', ]; //@codingStandardsIgnoreEnd @@ -330,8 +321,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity Product::COL_TYPE => 'product_type', Product::COL_PRODUCT_WEBSITES => 'product_websites', 'status' => 'product_online', - 'news_from_date' => self::COL_NEW_FROM_DATE, - 'news_to_date' => self::COL_NEW_TO_DATE, + 'news_from_date' => 'new_from_date', + 'news_to_date' => 'new_to_date', 'options_container' => 'display_product_options_in', 'minimal_price' => 'map_price', 'msrp' => 'msrp_price', @@ -795,6 +786,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param StockItemImporterInterface|null $stockItemImporter * @param DateTimeFactory $dateTimeFactory * @param ProductRepositoryInterface|null $productRepository + * @throws LocalizedException + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -913,7 +906,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ { if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $attrCode); + $this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode); } return false; } @@ -1254,6 +1247,7 @@ protected function _prepareRowForDb(array $rowData) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * phpcs:disable Generic.Metrics.NestingLevel */ protected function _saveLinks() { @@ -1263,7 +1257,7 @@ protected function _saveLinks() $nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable); // pre-load 'position' attributes ID for each link type once - foreach ($this->_linkNameToId as $linkName => $linkId) { + foreach ($this->_linkNameToId as $linkId) { $select = $this->_connection->select()->from( $resource->getTable('catalog_product_link_attribute'), ['id' => 'product_link_attribute_id'] @@ -1381,6 +1375,7 @@ protected function _saveLinks() } return $this; } + // phpcs:enable /** * Save product attributes. @@ -1615,6 +1610,7 @@ public function getImagesFromRow(array $rowData) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @throws LocalizedException + * phpcs:disable Generic.Metrics.NestingLevel */ protected function _saveProducts() { @@ -1646,11 +1642,8 @@ protected function _saveProducts() continue; } if ($this->getErrorAggregator()->hasToBeTerminated()) { - $validationStrategy = $this->_parameters[Import::FIELD_NAME_VALIDATION_STRATEGY]; - if (ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS !== $validationStrategy) { - $this->getErrorAggregator()->addRowToSkip($rowNum); - continue; - } + $this->getErrorAggregator()->addRowToSkip($rowNum); + continue; } $rowScope = $this->getRowScope($rowData); @@ -1658,7 +1651,7 @@ protected function _saveProducts() if (!empty($rowData[self::URL_KEY])) { // If url_key column and its value were in the CSV file $rowData[self::URL_KEY] = $urlKey; - } else if ($this->isNeedToChangeUrlKey($rowData)) { + } elseif ($this->isNeedToChangeUrlKey($rowData)) { // If url_key column was empty or even not declared in the CSV file but by the rules it is need to // be setteed. In case when url_key is generating from name column we have to ensure that the bunch // of products will pass for the event with url_key column. @@ -1670,7 +1663,9 @@ protected function _saveProducts() if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); continue; - } elseif (self::SCOPE_STORE == $rowScope) { + } + + if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; @@ -1988,6 +1983,7 @@ protected function _saveProducts() return $this; } + // phpcs:enable /** * Prepare array with image states (visible or hidden from product page) @@ -2436,6 +2432,7 @@ public function getRowScope(array $rowData) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Zend_Validate_Exception */ public function validateRow(array $rowData, $rowNum) { @@ -2451,32 +2448,35 @@ public function validateRow(array $rowData, $rowNum) // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } } if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } return true; } + // if product doesn't exist, need to throw critical error else all errors should be not critical. + $errorLevel = $this->getValidationErrorLevel($sku); + if (!$this->validator->isValid($rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $this->validator->getInvalidAttribute()); + $this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute()); } } if (null === $sku) { - $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel); } elseif (false === $sku) { - $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel); } elseif (self::SCOPE_STORE == $rowScope && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_STORE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel); } // SKU is specified, row is SCOPE_DEFAULT, new product block begins @@ -2491,16 +2491,15 @@ public function validateRow(array $rowData, $rowNum) $this->prepareNewSkuData($sku) ); } else { - $this->addRowError(ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel); } } else { // validate new product type and attribute set - if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum); - } elseif (!isset($rowData[self::COL_ATTR_SET]) - || !isset($this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) + if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) { + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel); + } elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel); } elseif ($this->skuProcessor->getNewSku($sku) === null) { $this->skuProcessor->addNewSku( $sku, @@ -2556,21 +2555,25 @@ public function validateRow(array $rowData, $rowNum) ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, $rowData[self::COL_NAME], - $message - ); + $message, + $errorLevel + ) + ->getErrorAggregator() + ->addRowToSkip($rowNum); } } } - if (!empty($rowData[self::COL_NEW_FROM_DATE]) && !empty($rowData[self::COL_NEW_TO_DATE]) + if (!empty($rowData['new_from_date']) && !empty($rowData['new_to_date']) ) { - $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData[self::COL_NEW_FROM_DATE], false)); - $newToTimestamp = strtotime($this->dateTime->formatDate($rowData[self::COL_NEW_TO_DATE], false)); + $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData['new_from_date'], false)); + $newToTimestamp = strtotime($this->dateTime->formatDate($rowData['new_to_date'], false)); if ($newFromTimestamp > $newToTimestamp) { - $this->addRowError( - ValidatorInterface::ERROR_NEW_TO_DATE, + $this->skipRow( $rowNum, - $rowData[self::COL_NEW_TO_DATE] + 'invalidNewToDateValue', + $errorLevel, + $rowData['new_to_date'] ); } } @@ -2588,8 +2591,8 @@ private function isNeedToValidateUrlKey($rowData) { return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME])) && (empty($rowData[self::COL_VISIBILITY]) - || $rowData[self::COL_VISIBILITY] - !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); + || $rowData[self::COL_VISIBILITY] + !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); } /** @@ -3067,9 +3070,7 @@ private function formatStockDataForRow(array $rowData): array if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { $stockItemDo->setData($row); - $row['is_in_stock'] = isset($row['is_in_stock']) && $stockItemDo->getBackorders() - ? $row['is_in_stock'] - : $this->stockStateProvider->verifyStock($stockItemDo); + $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); if ($this->stockStateProvider->verifyNotification($stockItemDo)) { $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT); @@ -3097,4 +3098,38 @@ private function retrieveProductBySku($sku) } return $product; } + + /** + * Add row as skipped + * + * @param int $rowNum + * @param string $errorCode Error code or simply column name + * @param string $errorLevel error level + * @param string|null $colName optional column name + * @return $this + */ + private function skipRow( + $rowNum, + string $errorCode, + string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL, + $colName = null + ): self { + $this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel); + $this->getErrorAggregator() + ->addRowToSkip($rowNum); + return $this; + } + + /** + * Returns errorLevel for validation + * + * @param string $sku + * @return string + */ + private function getValidationErrorLevel($sku): string + { + return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) + ? ProcessingError::ERROR_LEVEL_CRITICAL + : ProcessingError::ERROR_LEVEL_NOT_CRITICAL; + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index fa2853c738624..7435c0bebfc14 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -431,7 +431,10 @@ protected function _initMessageTemplates() ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_TYPE, - __('Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\'') + __( + 'Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: %1', + '\''.implode('\', \'', array_keys($this->_specificTypes)).'\'' + ) ); $this->_productEntity->addMessageTemplate(self::ERROR_EMPTY_TITLE, __('Please enter a value for title.')); $this->_productEntity->addMessageTemplate( @@ -629,7 +632,7 @@ public function validateAmbiguousData() $this->_addRowsErrors(self::ERROR_AMBIGUOUS_NEW_NAMES, $errorRows); return false; } - if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior() == Import::BEHAVIOR_APPEND) { $errorRows = $this->_findOldOptionsWithTheSameTitles(); if ($errorRows) { $this->_addRowsErrors(self::ERROR_AMBIGUOUS_OLD_NAMES, $errorRows); @@ -967,11 +970,10 @@ public function validateRow(array $rowData, $rowNumber) return false; } } - return true; } } - return false; + return true; } /** @@ -1381,7 +1383,7 @@ private function setLastOptionTitle(array &$titles) : void */ private function removeExistingOptions(array $products, array $optionsToRemove): void { - if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior() != Import::BEHAVIOR_APPEND) { $this->_deleteEntities(array_keys($products)); } elseif (!empty($optionsToRemove)) { // Remove options for products with empty "custom_options" row @@ -2108,7 +2110,7 @@ private function savePreparedCustomOptions( array $types ): void { if ($this->_isReadyForSaving($options, $titles, $types['values'])) { - if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior() == Import::BEHAVIOR_APPEND) { $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php index cbdc5f5beaaf9..f41596ad185a6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/RowValidatorInterface.php @@ -87,8 +87,6 @@ interface RowValidatorInterface extends \Magento\Framework\Validator\ValidatorIn const ERROR_DUPLICATE_MULTISELECT_VALUES = 'duplicatedMultiselectValues'; - const ERROR_NEW_TO_DATE = 'invalidNewToDateValue'; - /** * Value that means all entities (e.g. websites, groups etc.) */ diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 7e6ada724a81a..3ac7f98818d70 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -180,9 +180,9 @@ public function move($fileName, $renameFileOff = false) } $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); - $filePath = $this->_directory->getRelativePath($filePath . $fileName); + $relativePath = $this->_directory->getRelativePath($filePath . $fileName); $this->_directory->writeFile( - $filePath, + $relativePath, $read->readAll() ); } diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml new file mode 100644 index 0000000000000..b9eea2b114634 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <!-- Export products using filtering by attribute --> + <actionGroup name="exportProductsFilterByAttribute"> + <arguments> + <argument name="attribute" type="string"/> + <argument name="attributeData" type="string"/> + </arguments> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> + <scrollTo selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="scrollToAttribute" /> + <checkOption selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="selectAttribute"/> + <fillField selector="{{AdminExportAttributeSection.fillFilter('attribute')}}" userInput="{{attributeData}}" stepKey="setDataInField"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue" /> + <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Export products without filtering --> + <actionGroup name="exportAllProducts"> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible" time="5"/> + <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue"/> + <wait stepKey="waitForScroll" time="5"/> + <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> + <wait stepKey="waitForClick" time="5"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Download first file in the grid --> + <actionGroup name="downloadFileByRowIndex"> + <arguments> + <argument name="rowIndex" type="string"/> + </arguments> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormReload"/> + <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> + <click stepKey="clickOnDownload" selector="{{AdminExportAttributeSection.download(rowIndex)}}" after="clickSelectBtn"/> + </actionGroup> + + <!-- Delete exported file --> + <actionGroup name="deleteExportedFile"> + <arguments> + <argument name="rowIndex" type="string"/> + </arguments> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormReload"/> + <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> + <click stepKey="clickOnDelete" selector="{{AdminExportAttributeSection.delete(rowIndex)}}" after="clickSelectBtn"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForExportDataDeleted" /> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml new file mode 100644 index 0000000000000..1f5ae6b6905bc --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml @@ -0,0 +1,126 @@ +<?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="AdminExportBundleProductTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Bundle Product"/> + <description value="Admin should be able to export Bundle Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14008"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Create bundle product with dynamic price with two simple products --> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForDynamic"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForDynamic"/> + <createData entity="ApiBundleProduct" stepKey="createDynamicBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="firstSimpleProductForDynamic"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="secondSimpleProductForDynamic"/> + </createData> + + <!-- Create bundle product with fixed price with two simple products --> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixed"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixed"/> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="firstSimpleProductForFixed"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="secondSimpleProductForFixed"/> + </createData> + + <!-- Create bundle product with custom textarea attribute with two simple products --> + <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProductWithAttribute"> + <requiredEntity createDataKey="addToDefaultAttributeSet"/> + </createData> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixedWithAttribute"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixedWithAttribute"/> + <createData entity="DropDownBundleOption" stepKey="createBundleOptionWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProductWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + <requiredEntity createDataKey="createBundleOptionWithAttribute"/> + <requiredEntity createDataKey="firstSimpleProductForFixedWithAttribute"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProductWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + <requiredEntity createDataKey="createBundleOptionWithAttribute"/> + <requiredEntity createDataKey="secondSimpleProductForFixedWithAttribute"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete products creations --> + <deleteData createDataKey="createDynamicBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="firstSimpleProductForDynamic" stepKey="deleteFirstSimpleProductForDynamic"/> + <deleteData createDataKey="secondSimpleProductForDynamic" stepKey="deleteSecondSimpleProductForDynamic"/> + <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteFixedBundleProduct"/> + <deleteData createDataKey="firstSimpleProductForFixed" stepKey="deleteFirstSimpleProductForFixed"/> + <deleteData createDataKey="secondSimpleProductForFixed" stepKey="deleteSecondSimpleProductForFixed"/> + <deleteData createDataKey="createFixedBundleProductWithAttribute" stepKey="deleteFixedBundleProductWithAttribute"/> + <deleteData createDataKey="firstSimpleProductForFixedWithAttribute" stepKey="deleteFirstSimpleProductForFixedWithAttribute"/> + <deleteData createDataKey="secondSimpleProductForFixedWithAttribute" stepKey="deleteSecondSimpleProductForFixedWithAttribute"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml new file mode 100644 index 0000000000000..a587d71ba0e68 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -0,0 +1,91 @@ +<?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="AdminExportGroupedProductWithSpecialPriceTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export grouped product with special price"/> + <description value="Admin should be able to export grouped product with special price"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14009"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create first simple product and add special price --> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="specialProductPrice2" stepKey="specialPriceToFirstProduct"> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + + <!-- Create second simple product and add special price--> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="specialProductPrice2" stepKey="specialPriceToSecondProduct"> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create group product with created below simple products --> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </updateData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Deleted created products --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml new file mode 100644 index 0000000000000..6f64da4693692 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -0,0 +1,118 @@ +<?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="AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple and Configurable products with custom options"/> + <description value="Admin should be able to export Simple and Configurable products with custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14005"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create configurable product with two attributes --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Add custom options to configurable product --> + <updateData createDataKey="createConfigProduct" entity="productWithOptions" stepKey="updateProductWithOptions"/> + + <!-- Create two simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add created below children products to configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Fill entity attributes data --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml new file mode 100644 index 0000000000000..993f1c9cd9da2 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -0,0 +1,134 @@ +<?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="AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple product and Configurable products with assigned images"/> + <description value="Admin should be able to export Simple and Configurable products with assigned images"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14004"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create configurable product with two attributes --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create first simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + + <!-- Add image to first simple product --> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigChildFirstProductImage"> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + + <!-- Create second simple product which will be the part of configurable product --> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add image to second simple product --> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createConfigSecondChildProductImage"> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Add two options to configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add created below children products to configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Add image to configurable product --> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigProductImage"> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Fill entity attributes data --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml new file mode 100644 index 0000000000000..491d20604a08b --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml @@ -0,0 +1,116 @@ +<?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="AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> + <description value="Admin should be able to export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14006"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create configurable product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create two simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete simple product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml new file mode 100644 index 0000000000000..f671b54803e35 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -0,0 +1,68 @@ +<?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="AdminExportSimpleProductWithCustomAttributeTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple Product with custom attribute"/> + <description value="Admin should be able to export Simple Product with custom attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14007"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create simple product with custom attribute set --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="SimpleProductWithCustomAttributeSet" stepKey="createSimpleProductWithCustomAttributeSet"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createAttributeSet"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete product creations --> + <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index 98e434f217484..f0a52a67e0095 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -702,7 +702,7 @@ public function testValidateRowNoCustomOption() { $rowData = include __DIR__ . '/_files/row_data_no_custom_option.php'; $this->_bypassModelMethodGetMultiRowFormat($rowData); - $this->assertFalse($this->modelMock->validateRow($rowData, 0)); + $this->assertTrue($this->modelMock->validateRow($rowData, 0)); } /** diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index a562916b3b4bb..f85d33edb5d8c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Test\Unit\Model\Import; +use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import; +use PHPUnit\Framework\MockObject\MockObject; /** * Class ProductTest @@ -26,126 +29,126 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI const ENTITY_ID = 13; - /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\DB\Adapter\AdapterInterface| MockObject */ protected $_connection; - /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Json\Helper\Data| MockObject */ protected $jsonHelper; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data| MockObject */ protected $_dataSourceModel; - /** @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\App\ResourceConnection| MockObject */ protected $resource; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper| MockObject */ protected $_resourceHelper; - /** @var \Magento\Framework\Stdlib\StringUtils|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\StringUtils|MockObject */ protected $string; - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Event\ManagerInterface|MockObject */ protected $_eventManager; - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|MockObject */ protected $stockRegistry; - /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|MockObject */ protected $optionFactory; - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject */ protected $stockConfiguration; - /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|MockObject */ protected $stockStateProvider; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ protected $optionEntity; - /** @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime|MockObject */ protected $dateTime; /** @var array */ protected $data; - /** @var \Magento\ImportExport\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Helper\Data|MockObject */ protected $importExportData; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|MockObject */ protected $importData; - /** @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\Config|MockObject */ protected $config; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper|MockObject */ protected $resourceHelper; - /** @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Helper\Data|MockObject */ protected $_catalogData; - /** @var \Magento\ImportExport\Model\Import\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\Import\Config|MockObject */ protected $_importConfig; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var MockObject */ protected $_resourceFactory; // @codingStandardsIgnoreStart - /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|MockObject */ protected $_setColFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|MockObject */ protected $_productTypeFactory; - /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|MockObject */ protected $_linkFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|MockObject */ protected $_proxyProdFactory; - /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|MockObject */ protected $_uploaderFactory; - /** @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem|MockObject */ protected $_filesystem; - /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|MockObject */ protected $_mediaDirectory; - /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|MockObject */ protected $_stockResItemFac; - /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|MockObject */ protected $_localeDate; - /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Indexer\IndexerRegistry|MockObject */ protected $indexerRegistry; - /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Psr\Log\LoggerInterface|MockObject */ protected $_logger; - /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|MockObject */ protected $storeResolver; - /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|MockObject */ protected $skuProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|MockObject */ protected $categoryProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ protected $validator; - /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|MockObject */ protected $objectRelationProcessor; - /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|MockObject */ protected $transactionManager; - /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|MockObject */ // @codingStandardsIgnoreEnd protected $taxClassProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product */ + /** @var Product */ protected $importProduct; /** @@ -153,13 +156,13 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI */ protected $errorAggregator; - /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Framework\App\Config\ScopeConfigInterface|MockObject */ protected $scopeConfig; - /** @var \Magento\Catalog\Model\Product\Url|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Catalog\Model\Product\Url|MockObject */ protected $productUrl; - /** @var ImageTypeProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var ImageTypeProcessor|MockObject */ protected $imageTypeProcessor; /** @@ -343,7 +346,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->importProduct = $objectManager->getObject( - \Magento\CatalogImportExport\Model\Import\Product::class, + Product::class, [ 'jsonHelper' => $this->jsonHelper, 'importExportData' => $this->importExportData, @@ -385,7 +388,7 @@ protected function setUp() 'imageTypeProcessor' => $this->imageTypeProcessor ] ); - $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product::class); + $reflection = new \ReflectionClass(Product::class); $reflectionProperty = $reflection->getProperty('metadataPool'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->importProduct, $metadataPoolMock); @@ -627,7 +630,7 @@ public function testGetEmptyAttributeValueConstantFromParameters() public function testDeleteProductsForReplacement() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods([ 'setParameters', '_deleteProducts' @@ -693,7 +696,7 @@ public function testValidateRowIsAlreadyValidated() */ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour = Import::BEHAVIOR_DELETE) { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getBehavior', 'getRowScope', 'getErrorAggregator']) ->getMock(); @@ -705,7 +708,7 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour ->method('getErrorAggregator') ->willReturn($this->getErrorAggregatorObject()); $importProduct->expects($this->once())->method('getRowScope')->willReturn($rowScope); - $skuKey = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; + $skuKey = Product::COL_SKU; $rowData = [ $skuKey => 'sku', ]; @@ -717,18 +720,22 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour public function testValidateRowDeleteBehaviourAddRowErrorCall() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getBehavior', 'getRowScope', 'addRowError']) + ->setMethods(['getBehavior', 'getRowScope', 'addRowError', 'getErrorAggregator']) ->getMock(); $importProduct->expects($this->exactly(2))->method('getBehavior') ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); $importProduct->expects($this->once())->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); + ->willReturn(Product::SCOPE_DEFAULT); $importProduct->expects($this->once())->method('addRowError'); + $importProduct->method('getErrorAggregator') + ->willReturn( + $this->getErrorAggregatorObject(['addRowToSkip']) + ); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $importProduct->validateRow($rowData, 0); @@ -739,7 +746,7 @@ public function testValidateRowValidatorCheck() $messages = ['validator message']; $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $rowNum = 0; $this->importProduct->validateRow($rowData, $rowNum); @@ -841,7 +848,7 @@ public function getStoreIdByCodeDataProvider() return [ [ '$storeCode' => null, - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$expectedResult' => Product::SCOPE_DEFAULT, ], [ '$storeCode' => 'value', @@ -856,17 +863,17 @@ public function getStoreIdByCodeDataProvider() public function testValidateRowCheckSpecifiedSku($sku, $expectedError) { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity', 'getRowScope'], + ['addRowError', 'getOptionEntity', 'getRowScope'], ['isRowInvalid' => true] ); $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_STORE => '', + Product::COL_SKU => $sku, + Product::COL_STORE => '', ]; - $this->storeResolver->expects($this->any())->method('getStoreCodeToId')->willReturn(null); + $this->storeResolver->method('getStoreCodeToId')->willReturn(null); $this->setPropertyValue($importProduct, 'storeResolver', $this->storeResolver); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); @@ -875,7 +882,7 @@ public function testValidateRowCheckSpecifiedSku($sku, $expectedError) $importProduct ->expects($this->once()) ->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE); + ->willReturn(Product::SCOPE_STORE); $importProduct->expects($this->at(1))->method('addRowError')->with($expectedError, $rowNum)->willReturn(null); $importProduct->validateRow($rowData, $rowNum); @@ -889,7 +896,7 @@ public function testValidateRowProcessEntityIncrement() $errorAggregator->method('isRowInvalid')->willReturn(true); $this->setPropertyValue($this->importProduct, '_processedEntitiesCount', $count); $this->setPropertyValue($this->importProduct, 'errorAggregator', $errorAggregator); - $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => false]; + $rowData = [Product::COL_SKU => false]; //suppress validator $this->_setValidatorMockInImportProduct($this->importProduct); $this->importProduct->validateRow($rowData, $rowNum); @@ -899,14 +906,14 @@ public function testValidateRowProcessEntityIncrement() public function testValidateRowValidateExistingProductTypeAddNewSku() { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity'], + ['addRowError', 'getOptionEntity'], ['isRowInvalid' => true] ); $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -929,7 +936,7 @@ public function testValidateRowValidateExistingProductTypeAddNewSku() $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $expectedData = [ - 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val + 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val 'type_id' => $oldSku[$sku]['type_id'],// type_id_val 'attr_set_id' => $oldSku[$sku]['attr_set_id'], //attr_set_id_val 'attr_set_code' => $_attrSetIdToName[$oldSku[$sku]['attr_set_id']],//attr_set_id_val @@ -947,7 +954,7 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -972,6 +979,11 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() /** * @dataProvider validateRowValidateNewProductTypeAddRowErrorCallDataProvider + * @param string $colType + * @param string $productTypeModelsColType + * @param string $colAttrSet + * @param string $attrSetNameToIdColAttrSet + * @param string $error */ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $colType, @@ -983,15 +995,15 @@ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => $colType, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $colAttrSet, + Product::COL_SKU => $sku, + Product::COL_TYPE => $colType, + Product::COL_ATTR_SET => $colAttrSet, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, + $rowData[Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => $productTypeModelsColType, + $rowData[Product::COL_TYPE] => $productTypeModelsColType, ]; $oldSku = [ $sku => null, @@ -1019,29 +1031,25 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => 'value', - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'value', + Product::COL_SKU => $sku, + Product::COL_TYPE => 'value', + Product::COL_ATTR_SET => 'value', ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => 'value', + $rowData[Product::COL_TYPE] => 'value', ]; $oldSku = [ $sku => null, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => 'attr_set_code_val' + $rowData[Product::COL_ATTR_SET] => 'attr_set_code_val' ]; $expectedData = [ 'entity_id' => null, - 'type_id' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE],//value + 'type_id' => $rowData[Product::COL_TYPE],//value //attr_set_id_val - 'attr_set_id' => $_attrSetNameToId[ - $rowData[ - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET - ] - ], - 'attr_set_code' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET],//value + 'attr_set_id' => $_attrSetNameToId[$rowData[Product::COL_ATTR_SET]], + 'attr_set_code' => $rowData[Product::COL_ATTR_SET],//value 'row_id' => null ]; $importProduct = $this->createModelMockWithErrorAggregator( @@ -1077,8 +1085,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $expectedAttrSetCode = 'new_attr_set_code'; $newSku = [ @@ -1086,8 +1094,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() 'type_id' => 'new_type_id_val', ]; $expectedRowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $newSku['attr_set_code'], + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => $newSku['attr_set_code'], ]; $oldSku = [ $sku => [ @@ -1121,8 +1129,8 @@ public function testValidateValidateOptionEntity() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $oldSku = [ $sku => [ @@ -1374,7 +1382,7 @@ public function validateRowDataProvider() { return [ [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, ], @@ -1389,12 +1397,12 @@ public function validateRowDataProvider() '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => true, '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, '$behaviour' => Import::BEHAVIOR_REPLACE @@ -1415,7 +1423,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH - 1 + Product::DB_MAX_VARCHAR_LENGTH - 1 ), ], ], @@ -1468,7 +1476,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH - 1 + Product::DB_MAX_TEXT_LENGTH - 1 ), ], ], @@ -1488,7 +1496,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH + 1 + Product::DB_MAX_VARCHAR_LENGTH + 1 ), ], ], @@ -1541,7 +1549,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH + 1 + Product::DB_MAX_TEXT_LENGTH + 1 ), ], ], @@ -1553,8 +1561,8 @@ public function isAttributeValidAssertAttrInvalidDataProvider() */ public function getRowScopeDataProvider() { - $colSku = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; - $colStore = \Magento\CatalogImportExport\Model\Import\Product::COL_STORE; + $colSku = Product::COL_SKU; + $colStore = Product::COL_STORE; return [ [ @@ -1562,21 +1570,21 @@ public function getRowScopeDataProvider() $colSku => null, $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE ], [ '$rowData' => [ $colSku => 'sku', $colStore => null, ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT + '$expectedResult' => Product::SCOPE_DEFAULT ], [ '$rowData' => [ $colSku => 'sku', $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE ], ]; } @@ -1653,9 +1661,9 @@ protected function overrideMethod(&$object, $methodName, array $parameters = []) * * @see _rewriteGetOptionEntityInImportProduct() * @see _setValidatorMockInImportProduct() - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) { @@ -1671,8 +1679,8 @@ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) * Used in group of validateRow method's tests. * Set validator mock in importProduct, return true for isValid method. * - * @param \Magento\CatalogImportExport\Model\Import\Product - * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject + * @param Product + * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ private function _setValidatorMockInImportProduct($importProduct) { @@ -1686,9 +1694,9 @@ private function _setValidatorMockInImportProduct($importProduct) * Used in group of validateRow method's tests. * Make getOptionEntity return option mock. * - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _rewriteGetOptionEntityInImportProduct($importProduct) { @@ -1703,12 +1711,12 @@ private function _rewriteGetOptionEntityInImportProduct($importProduct) /** * @param array $methods * @param array $errorAggregatorMethods - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function createModelMockWithErrorAggregator(array $methods = [], array $errorAggregatorMethods = []) { $methods[] = 'getErrorAggregator'; - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods($methods) ->getMock(); diff --git a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php index 2c52b49c1a039..0a02d4eb6a9a6 100644 --- a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php +++ b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php @@ -7,6 +7,8 @@ namespace Magento\CatalogInventory\Model; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; /** * Catalog inventory module plugin @@ -17,18 +19,27 @@ class AddStockStatusToCollection * @var \Magento\CatalogInventory\Helper\Stock */ protected $stockHelper; - + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + /** - * @param \Magento\CatalogInventory\Model\Configuration $configuration * @param \Magento\CatalogInventory\Helper\Stock $stockHelper + * @param EngineResolverInterface $engineResolver */ public function __construct( - \Magento\CatalogInventory\Helper\Stock $stockHelper + \Magento\CatalogInventory\Helper\Stock $stockHelper, + EngineResolverInterface $engineResolver ) { $this->stockHelper = $stockHelper; + $this->engineResolver = $engineResolver; } /** + * Add stock filter to collection. + * * @param Collection $productCollection * @param bool $printQuery * @param bool $logQuery @@ -36,7 +47,9 @@ public function __construct( */ public function beforeLoad(Collection $productCollection, $printQuery = false, $logQuery = false) { - $this->stockHelper->addIsInStockFilterToCollection($productCollection); + if ($this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) { + $this->stockHelper->addIsInStockFilterToCollection($productCollection); + } return [$printQuery, $logQuery]; } } 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 bc10d38173b4d..43a5aabee9779 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -6,12 +6,19 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Model\Indexer\Stock\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement; +use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock; use Magento\Framework\App\ResourceConnection; use Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory; use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Framework\EntityManager\MetadataPool; @@ -25,7 +32,6 @@ /** * Class Full reindex action * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Full extends AbstractAction @@ -60,6 +66,11 @@ class Full extends AbstractAction */ private $activeTableSwitcher; + /** + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + /** * @param ResourceConnection $resource * @param StockFactory $indexerFactory @@ -71,7 +82,7 @@ class Full extends AbstractAction * @param BatchProviderInterface|null $batchProvider * @param array $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher - * + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -84,7 +95,8 @@ public function __construct( BatchSizeManagementInterface $batchSizeManagement = null, BatchProviderInterface $batchProvider = null, array $batchRowsCount = [], - ActiveTableSwitcher $activeTableSwitcher = null + ActiveTableSwitcher $activeTableSwitcher = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $resource, @@ -97,11 +109,12 @@ public function __construct( $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get(BatchProviderInterface::class); $this->batchSizeManagement = $batchSizeManagement ?: ObjectManager::getInstance()->get( - \Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement::class + BatchSizeManagement::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance() ->get(ActiveTableSwitcher::class); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -109,22 +122,20 @@ public function __construct( * * @param null|array $ids * @throws LocalizedException - * * @return void - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { try { $this->useIdxTable(false); $this->cleanIndexersTables($this->_getTypeIndexers()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys($this->_getConnection()->describeTable($this->_getIdxTable())); - /** @var \Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock $indexer */ + /** @var DefaultStock $indexer */ foreach ($this->_getTypeIndexers() as $indexer) { $indexer->setActionType(self::ACTION_TYPE); $connection = $indexer->getConnection(); @@ -135,22 +146,21 @@ public function execute($ids = null) : $this->batchRowsCount['default']; $this->batchSizeManagement->ensureBatchSize($connection, $batchRowCount); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $batchRowCount + $select, + $batchRowCount, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { + foreach ($batchQueries as $query) { $this->clearTemporaryIndexTable(); - // Get entity ids from batch - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $select->where('type_id = ?', $indexer->getTypeId()); - - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntity($entityIds); $select = $connection->select()->from($this->_getIdxTable(), $columns); @@ -167,12 +177,13 @@ public function execute($ids = null) /** * Delete all records from index table + * * Used to clean table before re-indexation * * @param array $indexers * @return void */ - private function cleanIndexersTables(array $indexers) + private function cleanIndexersTables(array $indexers): void { $tables = array_map( function (StockInterface $indexer) { diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php index b8e8e47bb1fb0..168e947b8fa57 100644 --- a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php +++ b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogInventory\Model\Plugin; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; + +/** + * Catalog inventory plugin for layer. + */ class Layer { /** @@ -21,16 +28,24 @@ class Layer */ protected $scopeConfig; + /** + * @var EngineResolverInterface + */ + private $engineResolver; + /** * @param \Magento\CatalogInventory\Helper\Stock $stockHelper * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param EngineResolverInterface $engineResolver */ public function __construct( \Magento\CatalogInventory\Helper\Stock $stockHelper, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + EngineResolverInterface $engineResolver ) { $this->stockHelper = $stockHelper; $this->scopeConfig = $scopeConfig; + $this->engineResolver = $engineResolver; } /** @@ -46,12 +61,22 @@ public function beforePrepareProductCollection( \Magento\Catalog\Model\Layer $subject, \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection ) { - if ($this->_isEnabledShowOutOfStock()) { + if (!$this->isCurrentEngineMysql() || $this->_isEnabledShowOutOfStock()) { return; } $this->stockHelper->addIsInStockFilterToCollection($collection); } + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + /** * Get config value for 'display out of stock' option * diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php index ce8930ad4f7a6..edccad60231ec 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php @@ -263,6 +263,12 @@ public function updateLowStockDate(int $websiteId) $connection->update($this->getMainTable(), $value, $where); } + /** + * Get Manage Stock Expression + * + * @param string $tableAlias + * @return \Zend_Db_Expr + */ public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr { if ($tableAlias) { @@ -277,6 +283,12 @@ public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr return $manageStock; } + /** + * Get Backorders Expression + * + * @param string $tableAlias + * @return \Zend_Db_Expr + */ public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr { if ($tableAlias) { @@ -291,6 +303,12 @@ public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr return $itemBackorders; } + /** + * Get Minimum Sale Quantity Expression + * + * @param string $tableAlias + * @return \Zend_Db_Expr + */ public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr { if ($tableAlias) { diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php index f711268bc7930..0fa4b919c40fa 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php @@ -7,16 +7,24 @@ /** * Interface StockRegistryProviderInterface + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockRegistryProviderInterface { /** + * Get stock. + * * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockInterface */ public function getStock($scopeId); /** + * Get stock item. + * * @param int $productId * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockItemInterface @@ -24,6 +32,8 @@ public function getStock($scopeId); public function getStockItem($productId, $scopeId); /** + * Get stock status. + * * @param int $productId * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockStatusInterface diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php index 89fb54e7e496b..30f703b5b928f 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php @@ -9,22 +9,32 @@ /** * Interface StockStateProviderInterface + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStateProviderInterface { /** + * Verify stock. + * * @param StockItemInterface $stockItem * @return bool */ public function verifyStock(StockItemInterface $stockItem); /** + * Verify notification. + * * @param StockItemInterface $stockItem * @return bool */ public function verifyNotification(StockItemInterface $stockItem); /** + * Validate quote qty. + * * @param StockItemInterface $stockItem * @param int|float $itemQty * @param int|float $qtyToCheck @@ -44,8 +54,9 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $itemQty, $qtyT public function checkQty(StockItemInterface $stockItem, $qty); /** - * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions - * or original qty if such value does not exist + * Returns suggested qty or original qty if such value does not exist. + * + * Suggested qty satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions. * * @param StockItemInterface $stockItem * @param int|float $qty @@ -54,6 +65,8 @@ public function checkQty(StockItemInterface $stockItem, $qty); public function suggestQty(StockItemInterface $stockItem, $qty); /** + * Check qty increments. + * * @param StockItemInterface $stockItem * @param int|float $qty * @return \Magento\Framework\DataObject diff --git a/app/code/Magento/CatalogInventory/Model/StockManagement.php b/app/code/Magento/CatalogInventory/Model/StockManagement.php index b3939f2e5149b..5d7d099dc01a0 100644 --- a/app/code/Magento/CatalogInventory/Model/StockManagement.php +++ b/app/code/Magento/CatalogInventory/Model/StockManagement.php @@ -85,6 +85,7 @@ public function __construct( /** * Subtract product qtys from stock. + * * Return array of items that require full save. * * @param string[] $items @@ -141,17 +142,25 @@ public function registerProductsSale($items, $websiteId = null) } /** - * @param string[] $items - * @param int $websiteId - * @return bool + * @inheritdoc */ public function revertProductsSale($items, $websiteId = null) { //if (!$websiteId) { $websiteId = $this->stockConfiguration->getDefaultScopeId(); //} - $this->qtyCounter->correctItemsQty($items, $websiteId, '+'); - return true; + $revertItems = []; + foreach ($items as $productId => $qty) { + $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); + $canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem); + if (!$canSubtractQty || !$this->stockConfiguration->isQty($stockItem->getTypeId())) { + continue; + } + $revertItems[$productId] = $qty; + } + $this->qtyCounter->correctItemsQty($revertItems, $websiteId, '+'); + + return $revertItems; } /** @@ -195,6 +204,8 @@ protected function getProductType($productId) } /** + * Get stock resource. + * * @return ResourceStock */ protected function getResource() diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php index 31fd5606a9849..6851b05aa56a6 100644 --- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php +++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php @@ -119,14 +119,12 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal()); if (!$stockItem->getIsQtyDecimal()) { $result->setHasQtyOptionUpdate(true); - $qty = (int) $qty; + $qty = (int) $qty ?: 1; /** * Adding stock data to quote item */ $result->setItemQty($qty); - $qty = $this->getNumber($qty); - $origQty = (int) $origQty; - $result->setOrigQty($origQty); + $result->setOrigQty((int)$this->getNumber($origQty) ?: 1); } if ($stockItem->getMinSaleQty() && $qty < $stockItem->getMinSaleQty()) { diff --git a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php index 1e99794d68a40..098e254d785a5 100644 --- a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php @@ -6,15 +6,21 @@ namespace Magento\CatalogInventory\Observer; -use Magento\Framework\Event\ObserverInterface; use Magento\CatalogInventory\Api\StockManagementInterface; +use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; /** * Catalog inventory module observer */ class CancelOrderItemObserver implements ObserverInterface { + /** + * @var \Magento\CatalogInventory\Model\Configuration + */ + protected $configuration; + /** * @var StockManagementInterface */ @@ -26,13 +32,16 @@ class CancelOrderItemObserver implements ObserverInterface protected $priceIndexer; /** + * @param Configuration $configuration * @param StockManagementInterface $stockManagement * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer */ public function __construct( + Configuration $configuration, StockManagementInterface $stockManagement, \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer ) { + $this->configuration = $configuration; $this->stockManagement = $stockManagement; $this->priceIndexer = $priceIndexer; } @@ -49,7 +58,8 @@ public function execute(EventObserver $observer) $item = $observer->getEvent()->getItem(); $children = $item->getChildrenItems(); $qty = $item->getQtyOrdered() - max($item->getQtyShipped(), $item->getQtyInvoiced()) - $item->getQtyCanceled(); - if ($item->getId() && $item->getProductId() && empty($children) && $qty) { + if ($item->getId() && $item->getProductId() && empty($children) && $qty && $this->configuration + ->getCanBackInStock()) { $this->stockManagement->backItemQty($item->getProductId(), $qty, $item->getStore()->getWebsiteId()); } $this->priceIndexer->reindexRow($item->getProductId()); diff --git a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php index 93a50cc9a7a4d..ab21f32b3f62c 100644 --- a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php @@ -64,8 +64,8 @@ public function execute(EventObserver $observer) { $quote = $observer->getEvent()->getQuote(); $items = $this->productQty->getProductQty($quote->getAllItems()); - $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); - $productIds = array_keys($items); + $revertedItems = $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); + $productIds = array_keys($revertedItems); if (!empty($productIds)) { $this->stockIndexerProcessor->reindexList($productIds); $this->priceIndexer->reindexList($productIds); diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php new file mode 100644 index 0000000000000..334d2b22edbfa --- /dev/null +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Plugin; + +use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; +use Magento\CatalogInventory\Api\Data\StockItemInterface; + +/** + * MassUpdate product attribute. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MassUpdateProductAttribute +{ + /** + * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + */ + private $stockIndexerProcessor; + + /** + * @var \Magento\Framework\Api\DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var \Magento\CatalogInventory\Api\StockRegistryInterface + */ + private $stockRegistry; + + /** + * @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var \Magento\CatalogInventory\Api\StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute + */ + private $attributeHelper; + + /** + * @var \Magento\Framework\Message\ManagerInterface + */ + private $messageManager; + + /** + * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor + * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration + * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper + * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, + \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, + \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, + \Magento\Framework\Message\ManagerInterface $messageManager + ) { + $this->stockIndexerProcessor = $stockIndexerProcessor; + $this->dataObjectHelper = $dataObjectHelper; + $this->stockRegistry = $stockRegistry; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + $this->attributeHelper = $attributeHelper; + $this->messageManager = $messageManager; + } + + /** + * Around execute plugin + * + * @param Save $subject + * @param callable $proceed + * + * @return \Magento\Framework\Controller\ResultInterface + */ + public function aroundExecute(Save $subject, callable $proceed) + { + try { + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = $subject->getRequest(); + $inventoryData = $request->getParam('inventory', []); + $inventoryData = $this->addConfigSettings($inventoryData); + + $storeId = $this->attributeHelper->getSelectedStoreId(); + $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); + $productIds = $this->attributeHelper->getProductIds(); + + if (!empty($inventoryData)) { + $this->updateInventoryInProducts($productIds, $websiteId, $inventoryData); + } + + return $proceed(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + return $proceed(); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while updating the product(s) attributes.') + ); + return $proceed(); + } + } + + /** + * Add config settings + * + * @param array $inventoryData + * + * @return array + */ + private function addConfigSettings($inventoryData) + { + $options = $this->stockConfiguration->getConfigItemOptions(); + foreach ($options as $option) { + $useConfig = 'use_config_' . $option; + if (isset($inventoryData[$option]) && !isset($inventoryData[$useConfig])) { + $inventoryData[$useConfig] = 0; + } + } + return $inventoryData; + } + + /** + * Update inventory in products + * + * @param array $productIds + * @param int $websiteId + * @param array $inventoryData + * + * @return void + */ + private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void + { + foreach ($productIds as $productId) { + $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); + if (!$stockItemDo->getProductId()) { + $inventoryData['product_id'] = $productId; + } + $stockItemId = $stockItemDo->getId(); + $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); + $stockItemDo->setItemId($stockItemId); + $this->stockItemRepository->save($stockItemDo); + } + $this->stockIndexerProcessor->reindexList($productIds); + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml index c7c9126f46803..2850b8d069201 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml @@ -21,6 +21,7 @@ <actionGroup name="noDisplayOutOfStockProduct"> <amOnPage url="{{InventoryConfigurationPage.url}}" stepKey="navigateToInventoryConfigurationPage"/> <waitForPageLoad stepKey="waitForConfigPageToLoad"/> + <conditionalClick stepKey="expandProductStockOptions" selector="{{InventoryConfigSection.ProductStockOptionsTab}}" dependentSelector="{{InventoryConfigSection.CheckIfProductStockOptionsTabExpanded}}" visible="true" /> <uncheckOption selector="{{InventoryConfigSection.DisplayOutOfStockSystemValue}}" stepKey="uncheckUseSystemValue"/> <waitForElementVisible selector="{{InventoryConfigSection.DisplayOutOfStockDropdown}}" stepKey="waitForSwitcherDropdown" /> <selectOption selector="{{InventoryConfigSection.DisplayOutOfStockDropdown}}" userInput="No" stepKey="switchToNo" /> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php index af35666ced3e5..906df54732775 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php @@ -6,6 +6,7 @@ namespace Magento\CatalogInventory\Test\Unit\Model; use Magento\CatalogInventory\Model\AddStockStatusToCollection; +use Magento\Framework\Search\EngineResolverInterface; class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase { @@ -19,13 +20,24 @@ class AddStockStatusToCollectionTest extends \PHPUnit\Framework\TestCase */ protected $stockHelper; + /** + * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $engineResolver; + protected function setUp() { $this->stockHelper = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); + $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrentSearchEngine']) + ->getMockForAbstractClass(); + $this->plugin = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( \Magento\CatalogInventory\Model\AddStockStatusToCollection::class, [ 'stockHelper' => $this->stockHelper, + 'engineResolver' => $this->engineResolver ] ); } @@ -36,6 +48,10 @@ public function testAddStockStatusToCollection() ->disableOriginalConstructor() ->getMock(); + $this->engineResolver->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('mysql'); + $this->stockHelper->expects($this->once()) ->method('addIsInStockFilterToCollection') ->with($productCollection) diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php index 287459bd8cbc8..b64563a35176d 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogInventory\Test\Unit\Model\Plugin; +use Magento\Framework\Search\EngineResolverInterface; + class LayerTest extends \PHPUnit\Framework\TestCase { /** @@ -22,14 +24,24 @@ class LayerTest extends \PHPUnit\Framework\TestCase */ protected $_stockHelperMock; + /** + * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $engineResolver; + protected function setUp() { $this->_scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->_stockHelperMock = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); + $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrentSearchEngine']) + ->getMockForAbstractClass(); $this->_model = new \Magento\CatalogInventory\Model\Plugin\Layer( $this->_stockHelperMock, - $this->_scopeConfigMock + $this->_scopeConfigMock, + $this->engineResolver ); } @@ -38,6 +50,10 @@ protected function setUp() */ public function testAddStockStatusDisabledShow() { + $this->engineResolver->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('mysql'); + $this->_scopeConfigMock->expects( $this->once() )->method( @@ -60,6 +76,10 @@ public function testAddStockStatusDisabledShow() */ public function testAddStockStatusEnabledShow() { + $this->engineResolver->expects($this->any()) + ->method('getCurrentSearchEngine') + ->willReturn('mysql'); + $this->_scopeConfigMock->expects( $this->once() )->method( diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php new file mode 100644 index 0000000000000..d66a783c6720d --- /dev/null +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Ui\DataProvider\Product; + +use Magento\Framework\Data\Collection; +use Magento\Ui\DataProvider\AddFieldToCollectionInterface; + +/** + * Add quantity_and_stock_status field to collection + */ +class AddQuantityAndStockStatusFieldToCollection implements AddFieldToCollectionInterface +{ + /** + * @inheritdoc + */ + public function addField(Collection $collection, $field, $alias = null) + { + $collection->joinField( + 'quantity_and_stock_status', + 'cataloginventory_stock_item', + 'is_in_stock', + 'product_id=entity_id', + '{{table}}.stock_id=1', + 'left' + ); + } +} diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index 007d744b2296f..eb6239ea87ef0 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -8,6 +8,7 @@ "php": "~7.1.3||~7.2.0", "magento/framework": "*", "magento/module-catalog": "*", + "magento/module-search": "*", "magento/module-config": "*", "magento/module-customer": "*", "magento/module-eav": "*", diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml index 3397ef25918cd..28035de29bc2e 100644 --- a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml @@ -23,6 +23,7 @@ <arguments> <argument name="addFieldStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFieldToCollection</item> + <item name="quantity_and_stock_status" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection</item> </argument> <argument name="addFilterStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFilterToCollection</item> diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index ace72bb11c37b..e7d79c593b8c7 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -44,7 +44,7 @@ </type> <type name="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"> <arguments> - <argument name="resourceStock" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Proxy</argument> + <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> </arguments> </type> <type name="Magento\Catalog\Model\Layer"> @@ -111,7 +111,7 @@ <argument name="batchSizeManagement" xsi:type="object">Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement</argument> </arguments> </type> - <type name="\Magento\Framework\Data\CollectionModifier"> + <type name="Magento\Framework\Data\CollectionModifier"> <arguments> <argument name="conditions" xsi:type="array"> <item name="stockStatusCondition" xsi:type="object">Magento\CatalogInventory\Model\ProductCollectionStockCondition</item> @@ -135,4 +135,7 @@ <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> </type> + <type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save"> + <plugin name="massAction" type="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute" /> + </type> </config> diff --git a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php index 5d93e6f216866..6b7c12dfdf463 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php @@ -10,6 +10,9 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Indexer\CacheContext; +/** + * Abstract class for CatalogRule indexers. + */ abstract class AbstractIndexer implements IndexerActionInterface, MviewActionInterface, IdentityInterface { /** @@ -66,7 +69,6 @@ public function executeFull() { $this->indexBuilder->reindexFull(); $this->_eventManager->dispatch('clean_cache_by_tags', ['object' => $this]); - //TODO: remove after fix fpc. MAGETWO-50668 $this->getCacheManager()->clean($this->getIdentities()); } @@ -137,8 +139,9 @@ public function executeRow($id) abstract protected function doExecuteRow($id); /** - * @return \Magento\Framework\App\CacheInterface|mixed + * Get cache manager * + * @return \Magento\Framework\App\CacheInterface|mixed * @deprecated 100.0.7 */ private function getCacheManager() diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php index 6d343fe149d21..fabe504fbe31c 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php @@ -71,6 +71,8 @@ public function mapConditionsToSearchCriteria(CombinedCondition $conditions): Se } /** + * Convert condition to filter group + * * @param ConditionInterface $condition * @return null|\Magento\Framework\Api\CombinedFilterGroup|\Magento\Framework\Api\Filter * @throws InputException @@ -89,6 +91,8 @@ private function mapConditionToFilterGroup(ConditionInterface $condition) } /** + * Convert combined condition to filter group + * * @param Combine $combinedCondition * @return null|\Magento\Framework\Api\CombinedFilterGroup * @throws InputException @@ -121,6 +125,8 @@ private function mapCombinedConditionToFilterGroup(CombinedCondition $combinedCo } /** + * Convert simple condition to filter group + * * @param ConditionInterface $productCondition * @return FilterGroup|Filter * @throws InputException @@ -139,6 +145,8 @@ private function mapSimpleConditionToFilterGroup(ConditionInterface $productCond } /** + * Convert simple condition with array value to filter group + * * @param ConditionInterface $productCondition * @return FilterGroup * @throws InputException @@ -161,6 +169,8 @@ private function processSimpleConditionWithArrayValue(ConditionInterface $produc } /** + * Get glue for multiple values by operator + * * @param string $operator * @return string */ @@ -211,6 +221,8 @@ private function reverseSqlOperatorInFilter(Filter $filter) } /** + * Convert filters array into combined filter group + * * @param array $filters * @param string $combinationMode * @return FilterGroup @@ -227,6 +239,8 @@ private function createCombinedFilterGroup(array $filters, string $combinationMo } /** + * Creating of filter object by filtering params + * * @param string $field * @param string $value * @param string $conditionType @@ -264,6 +278,7 @@ private function mapRuleOperatorToSQLCondition(string $ruleOperator): string '!{}' => 'nlike', // does not contains '()' => 'in', // is one of '!()' => 'nin', // is not one of + '<=>' => 'is_null' ]; if (!array_key_exists($ruleOperator, $operatorsMap)) { diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php index ab650c94a0f08..0db178b2a0a6d 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Catalog Rule Product Condition data model - */ namespace Magento\CatalogRule\Model\Rule\Condition; /** + * Catalog Rule Product Condition data model + * * @method string getAttribute() Returns attribute code */ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct @@ -29,6 +28,9 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) $oldAttrValue = $model->getData($attrCode); if ($oldAttrValue === null) { + if ($this->getOperator() === '<=>') { + return true; + } return false; } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml new file mode 100644 index 0000000000000..072e8b24b0336 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AdminOpenNewCatalogPriceRuleFormPageActionGroup"> + <amOnPage url="{{CatalogRuleNewPage.url}}" stepKey="openNewCatalogPriceRulePage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml new file mode 100644 index 0000000000000..93a2a8a610951 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.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="AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <grabMultiple selector="{{AdminNewCatalogPriceRule.customerGroupsOptions}}" stepKey="customerGroups" /> + <assertNotContains stepKey="assertCustomerGroupNotInOptions"> + <actualResult type="variable">customerGroups</actualResult> + <expectedResult type="string">{{customerGroup.code}}</expectedResult> + </assertNotContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index fe4042e8a2e9f..b0c4f2d8a609f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -36,6 +36,41 @@ <waitForPageLoad stepKey="waitForApplied"/> </actionGroup> + + <actionGroup name="createCatalogPriceRule"> + <arguments> + <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> + </arguments> + + <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> + <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName" /> + <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription" /> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite" /> + <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> + <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> + + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForPageLoad stepKey="waitForApplied"/> + </actionGroup> + + <actionGroup name="CreateCatalogPriceRuleConditionWithAttribute"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="targetValue" type="string"/> + <argument name="targetSelectValue" type="string"/> + </arguments> + + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="openConditionsTab"/> + <waitForPageLoad stepKey="waitForConditionTabOpened"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="addNewCondition"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="{{attributeName}}" stepKey="selectTypeCondition"/> + <waitForElement selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', targetValue)}}" stepKey="waitForIsTarget"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', 'is')}}" stepKey="clickOnIs"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.targetSelect('1')}}" userInput="{{targetSelectValue}}" stepKey="selectTargetCondition"/> + <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> + <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> + </actionGroup> + <!-- Apply all of the saved catalog price rules --> <actionGroup name="applyCatalogPriceRules"> <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> @@ -77,4 +112,8 @@ <actionGroup name="selectGeneralCustomerGroupActionGroup"> <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="General" stepKey="selectCustomerGroup"/> </actionGroup> + + <actionGroup name="selectNotLoggedInCustomerGroupActionGroup"> + <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..eb9cac1401c36 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?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="AdminMenuMarketing"> + <data key="pageTitle">Marketing</data> + <data key="title">Marketing</data> + <data key="dataUiId">magento-backend-marketing</data> + </entity> + <entity name="AdminMenuMarketingPromotionsCatalogPriceRule"> + <data key="pageTitle">Catalog Price Rule</data> + <data key="title">Catalog Price Rule</data> + <data key="dataUiId">magento-catalogrule-promo-catalog</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml index 71bdfe0613bb7..5b75708d1ae0a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -77,4 +77,21 @@ <data key="simple_action">by_percent</data> <data key="discount_amount">96</data> </entity> + + <entity name="CatalogRuleWithAllCustomerGroups" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRuleNewPage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRuleNewPage.xml new file mode 100644 index 0000000000000..ad3e40b37c5b0 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRuleNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CatalogRuleNewPage" url="catalog_rule/promo_catalog/new/" module="Magento_CatalogRule" area="admin"> + <section name="AdminNewCatalogPriceRule"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml index 7cfb5bf40be55..635260888e7fb 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml @@ -41,6 +41,8 @@ <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child"/> <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true"/> <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true"/> + <element name="targetEllipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="targetSelect" type="select" selector="//ul[@id='conditions__{{var}}__children']//select" parameterized="true" timeout="30"/> <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true"/> </section> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml new file mode 100644 index 0000000000000..053a8c33e640c --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -0,0 +1,136 @@ +<?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="AdminEnableAttributeIsUndefinedCatalogPriceRuleTest"> + <annotations> + <features value="CatalogRule"/> + <title value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> + <description value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13654"/> + <useCaseId value="MC-10971"/> + <group value="CatalogRule"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <createData entity="ApiCategory" stepKey="createFirstCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="productYesNoAttribute" stepKey="createProductAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + + <createData entity="SimpleSubCategory" stepKey="createSecondCategory"/> + <createData entity="SimpleProduct3" stepKey="createThirdProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + <createData entity="SimpleProduct4" stepKey="createForthProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + <createData entity="productDropDownAttribute" stepKey="createSecondProductAttribute"> + <field key="scope">website</field> + </createData> + </before> + <after> + + <!--Delete created data--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <click stepKey="resetFilters" selector="{{AdminSecondaryGridSection.resetFilters}}"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createFirstCategory" stepKey="deleteFirstCategory"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <deleteData createDataKey="createForthProduct" stepKey="deleteForthProduct"/> + <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> + <deleteData createDataKey="createSecondProductAttribute" stepKey="deleteSecondProductAttribute"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create catalog price rule--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> + <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup"/> + <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition"> + <argument name="attributeName" value="$$createProductAttribute.attribute[frontend_labels][0][label]$$"/> + <argument name="targetValue" value="is"/> + <argument name="targetSelectValue" value="is undefined"/> + </actionGroup> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Check Catalog Price Rule for first product--> + <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToFirstProductPage"/> + <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabFirstProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertFirstProductUpdatedPrice"/> + + <!--Check Catalog Price Rule for second product--> + <amOnPage url="{{StorefrontProductPage.url($$createSecondProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSecondProductPage"/> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabSecondProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertSecondProductUpdatedPrice"/> + + <!--Delete previous attribute and Catalog Price Rule--> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + + <!--Add new attribute to Default set--> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle1"> + <requiredEntity createDataKey="createSecondProductAttribute"/> + </createData> + + <!--Create new Catalog Price Rule--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> + <waitForPageLoad stepKey="waitForPriceRulePage1"/> + <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule1"> + <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup1"/> + <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition1"> + <argument name="attributeName" value="$$createSecondProductAttribute.attribute[frontend_labels][0][label]$$"/> + <argument name="targetValue" value="is"/> + <argument name="targetSelectValue" value="is undefined"/> + </actionGroup> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> + <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + + <!--Check Catalog Price Rule for third product--> + <amOnPage url="{{StorefrontProductPage.url($$createThirdProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToThirdProductPage"/> + <waitForPageLoad stepKey="waitForThirdProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabThirdProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabThirdProductUpdatedPrice)" stepKey="assertThirdProductUpdatedPrice"/> + + <!--Check Catalog Price Rule for forth product--> + <amOnPage url="{{StorefrontProductPage.url($$createForthProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToForthProductPage"/> + <waitForPageLoad stepKey="waitForForthProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabForthProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabForthProductUpdatedPrice)" stepKey="assertForthProductUpdatedPrice"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml new file mode 100644 index 0000000000000..0fe35419aaf3e --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingCatalogPriceRuleNavigateMenuTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing catalog price rule navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Catalog Price Rule"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14134"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogPriceRulePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingPromotionsCatalogPriceRule.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingPromotionsCatalogPriceRule.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml index cac7c94de446f..e3eac52a8d40b 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml @@ -64,7 +64,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> <see selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct"/> - <!--Login to storfront from customer and check price--> + <!--Login to storefront from customer and check price--> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> <argument name="Customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml new file mode 100644 index 0000000000000..75223fcfc4c4b --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -0,0 +1,17 @@ +<?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="DeleteCustomerGroupTest"> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRuleForm" /> + <actionGroup ref="AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup" stepKey="assertCustomerGroupNotOnCatalogPriceRuleForm"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/etc/db_schema.xml b/app/code/Magento/CatalogRule/etc/db_schema.xml index 6f7d713e49fe3..894f057ba73d1 100644 --- a/app/code/Magento/CatalogRule/etc/db_schema.xml +++ b/app/code/Magento/CatalogRule/etc/db_schema.xml @@ -23,7 +23,7 @@ <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="varchar" name="simple_action" nullable="true" length="32" comment="Simple Action"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> @@ -49,7 +49,7 @@ default="0" comment="Product Id"/> <column xsi:type="varchar" name="action_operator" nullable="true" length="10" default="to_fixed" comment="Action Operator"/> - <column xsi:type="decimal" name="action_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="action_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Action Amount"/> <column xsi:type="smallint" name="action_stop" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Action Stop"/> diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 35efe33461afc..9e5a1c866a842 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -16,7 +16,6 @@ <table name="catalog_product_entity" entity_column="entity_id" /> <table name="catalog_product_entity_datetime" entity_column="entity_id" /> <table name="catalog_product_entity_decimal" entity_column="entity_id" /> - <table name="catalog_product_entity_gallery" entity_column="entity_id" /> <table name="catalog_product_entity_int" entity_column="entity_id" /> <table name="catalog_product_entity_text" entity_column="entity_id" /> <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php index 184fd9cfd5b37..384132415a10e 100644 --- a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php @@ -60,15 +60,9 @@ public function execute() { try { $this->_catalogSearchAdvanced->addFilters($this->getRequest()->getQueryValue()); - $size = $this->_catalogSearchAdvanced->getProductCollection()->getSize(); - - $handles = null; - if ($size == 0) { - $this->_view->getPage()->initLayout(); - $handles = $this->_view->getLayout()->getUpdate()->getHandles(); - $handles[] = static::DEFAULT_NO_RESULT_HANDLE; - } - + $this->_view->getPage()->initLayout(); + $handles = $this->_view->getLayout()->getUpdate()->getHandles(); + $handles[] = static::DEFAULT_NO_RESULT_HANDLE; $this->_view->loadLayout($handles); $this->_view->renderLayout(); } catch (\Magento\Framework\Exception\LocalizedException $e) { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php index 15856bbee7461..66f5ad7a7192b 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php @@ -16,8 +16,11 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface; use Magento\Framework\Search\Request\BucketInterface; +use Magento\Framework\Event\Manager; /** + * Data Provider for catalog search. + * * @deprecated * @see \Magento\ElasticSearch */ @@ -43,12 +46,18 @@ class DataProvider implements DataProviderInterface */ private $selectBuilderForAttribute; + /** + * @var Manager + */ + private $eventManager; + /** * @param Config $eavConfig * @param ResourceConnection $resource * @param ScopeResolverInterface $scopeResolver * @param null $customerSession @deprecated * @param SelectBuilderForAttribute|null $selectBuilderForAttribute + * @param Manager|null $eventManager * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -57,17 +66,19 @@ public function __construct( ResourceConnection $resource, ScopeResolverInterface $scopeResolver, $customerSession, - SelectBuilderForAttribute $selectBuilderForAttribute = null + SelectBuilderForAttribute $selectBuilderForAttribute = null, + Manager $eventManager = null ) { $this->eavConfig = $eavConfig; $this->connection = $resource->getConnection(); $this->scopeResolver = $scopeResolver; $this->selectBuilderForAttribute = $selectBuilderForAttribute ?: ObjectManager::getInstance()->get(SelectBuilderForAttribute::class); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(Manager::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function getDataSet( BucketInterface $bucket, @@ -83,13 +94,17 @@ public function getDataSet( 'main_table.entity_id = entities.entity_id', [] ); + $this->eventManager->dispatch( + 'catalogsearch_query_add_filter_after', + ['bucket' => $bucket, 'select' => $select] + ); $select = $this->selectBuilderForAttribute->build($select, $attribute, $currentScope); return $select; } /** - * {@inheritdoc} + * @inheritdoc */ public function execute(Select $select) { @@ -97,6 +112,8 @@ public function execute(Select $select) } /** + * Get select. + * * @return Select */ private function getSelect() diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php index ddb4085fa13d9..00012a78d1003 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute.php @@ -74,6 +74,8 @@ public function __construct( } /** + * Build select for attribute search + * * @param Select $select * @param AbstractAttribute $attribute * @param int $currentScope @@ -101,7 +103,7 @@ public function build(Select $select, AbstractAttribute $attribute, int $current $subSelect = $select; $subSelect->from(['main_table' => $table], ['main_table.entity_id', 'main_table.value']) ->distinct() - ->where('main_table.attribute_id = ?', $attribute->getAttributeId()) + ->where('main_table.attribute_id = ?', (int) $attribute->getAttributeId()) ->where('main_table.store_id = ? ', $currentScopeId); if ($this->isAddStockFilter()) { $subSelect = $this->applyStockConditionToSelect->execute($subSelect); @@ -116,6 +118,8 @@ public function build(Select $select, AbstractAttribute $attribute, int $current } /** + * Is add stock filter + * * @return bool */ private function isAddStockFilter() diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index 2ffa63098cdee..c758e773f43c1 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -8,7 +8,9 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; use Magento\CatalogSearch\Model\Search\TableMapper; +use Magento\Customer\Model\Session; use Magento\Eav\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -20,10 +22,11 @@ use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface; use Magento\Framework\Search\Request\FilterInterface; use Magento\Store\Model\Store; -use Magento\Customer\Model\Session; -use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; /** + * ElasticSearch search filter pre-processor. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated * @see \Magento\ElasticSearch @@ -128,7 +131,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function process(FilterInterface $filter, $isNegation, $query) { @@ -136,10 +139,13 @@ public function process(FilterInterface $filter, $isNegation, $query) } /** + * Process query with field. + * * @param FilterInterface $filter * @param bool $isNegation * @param string $query * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ private function processQueryWithField(FilterInterface $filter, $isNegation, $query) { @@ -170,7 +176,7 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu } elseif ($filter->getField() === VisibilityFilter::VISIBILITY_FILTER_FIELD) { return ''; } elseif ($filter->getType() === FilterInterface::TYPE_TERM && - in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) + in_array($attribute->getFrontendInput(), ['select', 'multiselect', 'boolean'], true) ) { $resultQuery = $this->processTermSelect($filter, $isNegation); } elseif ($filter->getType() === FilterInterface::TYPE_RANGE && @@ -204,19 +210,23 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu ->where('main_table.store_id = ?', Store::DEFAULT_STORE_ID) ->having($query); - $resultQuery = 'search_index.entity_id IN ( - select entity_id from ' . $this->conditionManager->wrapBrackets($select) . ' as filter - )'; + $resultQuery = 'search_index.entity_id IN (' + . 'select entity_id from ' + . $this->conditionManager->wrapBrackets($select) + . ' as filter)'; } return $resultQuery; } /** + * Process range numeric. + * * @param FilterInterface $filter * @param string $query * @param Attribute $attribute * @return string + * @throws \Exception */ private function processRangeNumeric(FilterInterface $filter, $query, $attribute) { @@ -238,14 +248,17 @@ private function processRangeNumeric(FilterInterface $filter, $query, $attribute ->where('main_table.store_id = ?', $currentStoreId) ->having($query); - $resultQuery = 'search_index.entity_id IN ( - select entity_id from ' . $this->conditionManager->wrapBrackets($select) . ' as filter - )'; + $resultQuery = 'search_index.entity_id IN (' + . 'select entity_id from ' + . $this->conditionManager->wrapBrackets($select) + . ' as filter)'; return $resultQuery; } /** + * Process term select. + * * @param FilterInterface $filter * @param bool $isNegation * @return string diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index af0e9ff5528cf..5b96a8c21cbea 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -6,6 +6,8 @@ namespace Magento\CatalogSearch\Model; use Magento\Catalog\Model\Config; +use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider; +use Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; @@ -19,6 +21,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; /** * Catalog advanced search model @@ -64,6 +67,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Initialize dependencies * + * @deprecated * @var Config */ protected $_catalogConfig; @@ -106,10 +110,22 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Advanced Collection Factory * + * @deprecated + * @see $collectionProvider * @var ProductCollectionFactory */ protected $productCollectionFactory; + /** + * @var ItemCollectionProviderInterface + */ + private $collectionProvider; + + /** + * @var ProductCollectionPrepareStrategyProvider|null + */ + private $productCollectionPrepareStrategyProvider; + /** * Construct * @@ -124,7 +140,8 @@ class Advanced extends \Magento\Framework\Model\AbstractModel * @param ProductCollectionFactory $productCollectionFactory * @param AdvancedFactory $advancedFactory * @param array $data - * + * @param ItemCollectionProviderInterface|null $collectionProvider + * @param ProductCollectionPrepareStrategyProvider|null $productCollectionPrepareStrategyProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -138,7 +155,9 @@ public function __construct( StoreManagerInterface $storeManager, ProductCollectionFactory $productCollectionFactory, AdvancedFactory $advancedFactory, - array $data = [] + array $data = [], + ItemCollectionProviderInterface $collectionProvider = null, + ProductCollectionPrepareStrategyProvider $productCollectionPrepareStrategyProvider = null ) { $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_catalogProductVisibility = $catalogProductVisibility; @@ -147,11 +166,14 @@ public function __construct( $this->_productFactory = $productFactory; $this->_storeManager = $storeManager; $this->productCollectionFactory = $productCollectionFactory; + $this->collectionProvider = $collectionProvider; + $this->productCollectionPrepareStrategyProvider = $productCollectionPrepareStrategyProvider + ?: ObjectManager::getInstance()->get(ProductCollectionPrepareStrategyProvider::class); parent::__construct( $context, $registry, $advancedFactory->create(), - $this->productCollectionFactory->create(), + $this->resolveProductCollection(), $data ); } @@ -266,7 +288,7 @@ public function getAttributes() public function getProductCollection() { if ($this->_productCollection === null) { - $collection = $this->productCollectionFactory->create(); + $collection = $this->resolveProductCollection(); $this->prepareProductCollection($collection); if (!$collection) { return $collection; @@ -277,6 +299,18 @@ public function getProductCollection() return $this->_productCollection; } + /** + * Resolve product collection. + * + * @return \Magento\Catalog\Model\ResourceModel\Product\Collection|\Magento\Framework\Data\Collection + */ + private function resolveProductCollection() + { + return (null === $this->collectionProvider) + ? $this->productCollectionFactory->create() + : $this->collectionProvider->getCollection(); + } + /** * Prepare product collection * @@ -285,13 +319,7 @@ public function getProductCollection() */ public function prepareProductCollection($collection) { - $collection - ->addAttributeToSelect($this->_catalogConfig->getProductAttributes()) - ->setStore($this->_storeManager->getStore()) - ->addMinimalPrice() - ->addTaxPercents() - ->addStoreFilter() - ->setVisibility($this->_catalogProductVisibility->getVisibleInSearchIds()); + $this->productCollectionPrepareStrategyProvider->getStrategy()->prepare($collection); return $this; } diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php new file mode 100644 index 0000000000000..e62de9c689fc2 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Strategy interface for preparing product collection. + */ +class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface +{ + /** + * @var Config + */ + private $catalogConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Visibility + */ + private $catalogProductVisibility; + + /** + * @param Config $catalogConfig + * @param StoreManagerInterface $storeManager + * @param Visibility $catalogProductVisibility + */ + public function __construct( + Config $catalogConfig, + StoreManagerInterface $storeManager, + Visibility $catalogProductVisibility + ) { + $this->catalogConfig = $catalogConfig; + $this->storeManager = $storeManager; + $this->catalogProductVisibility = $catalogProductVisibility; + } + + /** + * @inheritdoc + */ + public function prepare(Collection $collection) + { + $collection + ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) + ->setStore($this->storeManager->getStore()) + ->addMinimalPrice() + ->addTaxPercents() + ->addStoreFilter() + ->setVisibility($this->catalogProductVisibility->getVisibleInSearchIds()); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php new file mode 100644 index 0000000000000..23719a6713a32 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; + +/** + * Strategy interface for preparing product collection. + */ +interface ProductCollectionPrepareStrategyInterface +{ + /** + * Prepare product collection. + * + * @param Collection $collection + * @return void + */ + public function prepare(Collection $collection); +} diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php new file mode 100644 index 0000000000000..6e963ea1aa8ac --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Advanced; + +use Magento\Framework\Search\EngineResolverInterface; + +/** + * Strategy provider for preparing product collection. + */ +class ProductCollectionPrepareStrategyProvider +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $strategies; + + /** + * @param EngineResolverInterface $engineResolver + * @param array $strategies + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $strategies + ) { + $this->engineResolver = $engineResolver; + $this->strategies = $strategies; + } + + /** + * Get strategy provider for product collection prepare process. + * + * @return ProductCollectionPrepareStrategyInterface + */ + public function getStrategy(): ProductCollectionPrepareStrategyInterface + { + if (!isset($this->strategies[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined strategy ' . $this->engineResolver->getCurrentSearchEngine()); + } + return $this->strategies[$this->engineResolver->getCurrentSearchEngine()]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index 7b239d84bf962..794d0ac971536 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -153,4 +153,12 @@ private function getOptionCount($value, $optionsFacetedData) ? (int)$optionsFacetedData[$value]['count'] : 0; } + + /** + * @inheritdoc + */ + protected function isOptionReducesResults($optionCount, $totalSize) + { + return $optionCount <= $totalSize; + } } diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php index e61a886a41d6f..e9fb1070fedd5 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php @@ -111,12 +111,9 @@ protected function _getItemsData() $from = ''; } if ($to == '*') { - $to = ''; + $to = null; } - $label = $this->renderRangeLabel( - empty($from) ? 0 : $from, - empty($to) ? 0 : $to - ); + $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); $value = $from . '-' . $to; $data[] = [ @@ -141,7 +138,7 @@ protected function _getItemsData() protected function renderRangeLabel($fromPrice, $toPrice) { $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($toPrice === null) { return __('%1 and above', $formattedFromPrice); } else { if ($fromPrice != $toPrice) { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index b4b15554f6029..7791dc761ae39 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -3,24 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; use Magento\Catalog\Model\Product; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\DB\Select; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\Api\Search\SearchResultFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Api\Search\SearchResultInterface; /** * Advanced search collection * * This collection should be refactored to not have dependencies on MySQL-specific implementation. * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -59,6 +71,41 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $filterBuilder; + /** + * @var \Magento\Framework\Api\Search\SearchResultInterface + */ + private $searchResult; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var SearchCriteriaResolverFactory + */ + private $searchCriteriaResolverFactory; + + /** + * @var SearchResultApplierFactory + */ + private $searchResultApplierFactory; + + /** + * @var TotalRecordsResolverFactory + */ + private $totalRecordsResolverFactory; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $searchOrders; + /** * Collection constructor * @@ -88,7 +135,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param string $searchRequestName + * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory + * @param SearchResultApplierFactory|null $searchResultApplierFactory + * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory + * @param EngineResolverInterface|null $engineResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -117,15 +168,29 @@ public function __construct( \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, SearchResultFactory $searchResultFactory = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + $searchRequestName = 'advanced_search_container', + SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, + SearchResultApplierFactory $searchResultApplierFactory = null, + TotalRecordsResolverFactory $totalRecordsResolverFactory = null, + EngineResolverInterface $engineResolver = null ) { $this->requestBuilder = $requestBuilder; $this->searchEngine = $searchEngine; $this->temporaryStorageFactory = $temporaryStorageFactory; + $this->searchRequestName = $searchRequestName; if ($searchResultFactory === null) { $this->searchResultFactory = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Api\Search\SearchResultFactory::class); } + $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() + ->get(SearchCriteriaResolverFactory::class); + $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() + ->get(SearchResultApplierFactory::class); + $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() + ->get(TotalRecordsResolverFactory::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() + ->get(EngineResolverInterface::class); parent::__construct( $entityFactory, $logger, @@ -167,11 +232,89 @@ public function addFieldsToFilter($fields) return $this; } + /** + * @inheritdoc + */ + public function setOrder($attribute, $dir = Select::SQL_DESC) + { + $this->setSearchOrder($attribute, $dir); + if ($this->isCurrentEngineMysql()) { + parent::setOrder($attribute, $dir); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function addCategoryFilter(\Magento\Catalog\Model\Category $category) + { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::addCategoryFilter($category); + } else { + $this->addFieldToFilter('category_ids', $category->getId()); + $this->_productLimitationPrice(); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function setVisibility($visibility) + { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::setVisibility($visibility); + } else { + $this->addFieldToFilter('visibility', $visibility); + } + + return $this; + } + + /** + * Set sort order for search query. + * + * @param string $field + * @param string $direction + * @return void + */ + private function setSearchOrder($field, $direction) + { + $field = (string)$this->_getMappedField($field); + $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; + + $this->searchOrders[$field] = $direction; + } + + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + /** * @inheritdoc */ protected function _renderFiltersBefore() { + if ($this->isLoaded()) { + return; + } if ($this->filters) { foreach ($this->filters as $attributes) { foreach ($attributes as $attributeCode => $attributeValue) { @@ -179,33 +322,70 @@ protected function _renderFiltersBefore() $this->addAttributeToSearch($attributeCode, $attributeValue); } } - $searchCriteria = $this->getSearchCriteriaBuilder()->create(); - $searchCriteria->setRequestName('advanced_search_container'); + $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); try { - $searchResult = $this->getSearch()->search($searchCriteria); + $this->searchResult = $this->getSearch()->search($searchCriteria); + $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); } catch (EmptyRequestDataException $e) { /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ - $searchResult = $this->searchResultFactory->create()->setItems([]); + $this->searchResult = $this->searchResultFactory->create()->setItems([]); } catch (NonExistingRequestNameException $e) { $this->_logger->error($e->getMessage()); throw new LocalizedException( __('An error occurred. For details, see the error log.') ); } - $temporaryStorage = $this->temporaryStorageFactory->create(); - $table = $temporaryStorage->storeApiDocuments($searchResult->getItems()); - - $this->getSelect()->joinInner( - [ - 'search_result' => $table->getName(), - ], - 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, - [] - ); + $this->getSearchResultApplier($this->searchResult)->apply(); } parent::_renderFiltersBefore(); } + /** + * Get total records resolver. + * + * @param SearchResultInterface $searchResult + * @return TotalRecordsResolverInterface + */ + private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface + { + return $this->totalRecordsResolverFactory->create([ + 'searchResult' => $searchResult, + ]); + } + + /** + * Get search criteria resolver. + * + * @return SearchCriteriaResolverInterface + */ + private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface + { + return $this->searchCriteriaResolverFactory->create([ + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => $this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, + ]); + } + + /** + * Get search result applier. + * + * @param SearchResultInterface $searchResult + * @return SearchResultApplierInterface + */ + private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface + { + return $this->searchResultApplierFactory->create([ + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders + ]); + } + /** * Get attribute code. * @@ -296,7 +476,7 @@ private function getSearchCriteriaBuilder() } /** - * Get fielter builder. + * Get filter builder. * * @return FilterBuilder */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php index 49caede8c4ac2..93ae2c94e2105 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php @@ -110,7 +110,9 @@ public function processAttributeValue($attribute, $value) && in_array($attribute->getFrontendInput(), ['text', 'textarea']) ) { $result = $value; - } elseif ($this->isTermFilterableAttribute($attribute)) { + } elseif ($this->isTermFilterableAttribute($attribute) + || ($attribute->getIsSearchable() && in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) + ) { $result = ''; } @@ -119,6 +121,7 @@ public function processAttributeValue($attribute, $value) /** * Prepare index array as a string glued by separator + * * Support 2 level array gluing * * @param array $index diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index e6cfe8ca112f7..59f6cd1c6e7eb 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -3,13 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext; -use Magento\CatalogSearch\Model\Search\RequestGenerator; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\Data\Collection\Db\SizeResolverInterfaceFactory; use Magento\Framework\DB\Select; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\StateException; -use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; use Magento\Framework\Search\Response\QueryResponse; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; @@ -17,6 +26,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Search\Model\EngineResolver; /** * Fulltext Collection @@ -26,6 +36,8 @@ * @api * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -60,11 +72,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $queryText; - /** - * @var string|null - */ - private $relevanceOrderDirection = null; - /** * @var string */ @@ -101,6 +108,31 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $filterBuilder; + /** + * @var SearchCriteriaResolverFactory + */ + private $searchCriteriaResolverFactory; + + /** + * @var SearchResultApplierFactory + */ + private $searchResultApplierFactory; + + /** + * @var TotalRecordsResolverFactory + */ + private $totalRecordsResolverFactory; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $searchOrders; + /** * Collection constructor * @@ -132,8 +164,15 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param \Magento\Search\Api\SearchInterface|null $search + * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder|null $filterBuilder + * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory + * @param SearchResultApplierFactory|null $searchResultApplierFactory + * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory + * @param EngineResolverInterface|null $engineResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -163,7 +202,14 @@ public function __construct( $searchRequestName = 'catalog_view_container', SearchResultFactory $searchResultFactory = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + \Magento\Search\Api\SearchInterface $search = null, + \Magento\Framework\Api\Search\SearchCriteriaBuilder $searchCriteriaBuilder = null, + \Magento\Framework\Api\FilterBuilder $filterBuilder = null, + SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, + SearchResultApplierFactory $searchResultApplierFactory = null, + TotalRecordsResolverFactory $totalRecordsResolverFactory = null, + EngineResolverInterface $engineResolver = null ) { $this->queryFactory = $catalogSearchData; if ($searchResultFactory === null) { @@ -198,6 +244,19 @@ public function __construct( $this->searchEngine = $searchEngine; $this->temporaryStorageFactory = $temporaryStorageFactory; $this->searchRequestName = $searchRequestName; + $this->search = $search ?: ObjectManager::getInstance()->get(\Magento\Search\Api\SearchInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Api\Search\SearchCriteriaBuilder::class); + $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Api\FilterBuilder::class); + $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() + ->get(SearchCriteriaResolverFactory::class); + $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() + ->get(SearchResultApplierFactory::class); + $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() + ->get(TotalRecordsResolverFactory::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() + ->get(EngineResolverInterface::class); } /** @@ -331,32 +390,32 @@ public function addSearchFilter($query) /** * @inheritdoc */ - protected function _renderFiltersBefore() + public function setOrder($attribute, $dir = Select::SQL_DESC) { - $this->getSearchCriteriaBuilder(); - $this->getFilterBuilder(); - $this->getSearch(); - - if ($this->queryText) { - $this->filterBuilder->setField('search_term'); - $this->filterBuilder->setValue($this->queryText); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + $this->setSearchOrder($attribute, $dir); + if ($this->isCurrentEngineMysql()) { + parent::setOrder($attribute, $dir); } - $priceRangeCalculation = $this->_scopeConfig->getValue( - \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - if ($priceRangeCalculation) { - $this->filterBuilder->setField('price_dynamic_algorithm'); - $this->filterBuilder->setValue($priceRangeCalculation); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + return $this; + } + + /** + * @inheritdoc + */ + protected function _renderFiltersBefore() + { + if ($this->isLoaded()) { + return; } - $searchCriteria = $this->searchCriteriaBuilder->create(); - $searchCriteria->setRequestName($this->searchRequestName); + $this->prepareSearchTermFilter(); + $this->preparePriceAggregation(); + + $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); try { $this->searchResult = $this->getSearch()->search($searchCriteria); + $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); } catch (EmptyRequestDataException $e) { /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ $this->searchResult = $this->searchResultFactory->create()->setItems([]); @@ -365,23 +424,79 @@ protected function _renderFiltersBefore() throw new LocalizedException(__('An error occurred. For details, see the error log.')); } - $temporaryStorage = $this->temporaryStorageFactory->create(); - $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); + $this->getSearchResultApplier($this->searchResult)->apply(); + parent::_renderFiltersBefore(); + } - $this->getSelect()->joinInner( - [ - 'search_result' => $table->getName(), - ], - 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, - [] - ); + /** + * Set sort order for search query. + * + * @param string $field + * @param string $direction + * @return void + */ + private function setSearchOrder($field, $direction) + { + $field = (string)$this->_getMappedField($field); + $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; - if ($this->relevanceOrderDirection) { - $this->getSelect()->order( - 'search_result.'. TemporaryStorage::FIELD_SCORE . ' ' . $this->relevanceOrderDirection - ); - } - return parent::_renderFiltersBefore(); + $this->searchOrders[$field] = $direction; + } + + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + + /** + * Get total records resolver. + * + * @param SearchResultInterface $searchResult + * @return TotalRecordsResolverInterface + */ + private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface + { + return $this->totalRecordsResolverFactory->create([ + 'searchResult' => $searchResult, + ]); + } + + /** + * Get search criteria resolver. + * + * @return SearchCriteriaResolverInterface + */ + private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface + { + return $this->searchCriteriaResolverFactory->create([ + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => $this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, + ]); + } + + /** + * Get search result applier. + * + * @param SearchResultInterface $searchResult + * @return SearchResultApplierInterface + */ + private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface + { + return $this->searchResultApplierFactory->create([ + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders, + ]); } /** @@ -409,24 +524,6 @@ protected function _renderFilters() return parent::_renderFilters(); } - /** - * Set Order field - * - * @param string $attribute - * @param string $dir - * @return $this - */ - public function setOrder($attribute, $dir = Select::SQL_DESC) - { - if ($attribute === 'relevance') { - $this->relevanceOrderDirection = $dir; - } else { - parent::setOrder($attribute, $dir); - } - - return $this; - } - /** * Stub method for compatibility with other search engines * @@ -473,7 +570,17 @@ public function getFacetedData($field) public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { $this->addFieldToFilter('category_ids', $category->getId()); - return parent::addCategoryFilter($category); + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::addCategoryFilter($category); + } else { + $this->_productLimitationPrice(); + } + + return $this; } /** @@ -485,6 +592,46 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) public function setVisibility($visibility) { $this->addFieldToFilter('visibility', $visibility); - return parent::setVisibility($visibility); + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::setVisibility($visibility); + } + + return $this; + } + + /** + * Prepare search term filter for text query. + * + * @return void + */ + private function prepareSearchTermFilter(): void + { + if ($this->queryText) { + $this->filterBuilder->setField('search_term'); + $this->filterBuilder->setValue($this->queryText); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + } + } + + /** + * Prepare price aggregation algorithm. + * + * @return void + */ + private function preparePriceAggregation(): void + { + $priceRangeCalculation = $this->_scopeConfig->getValue( + \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($priceRangeCalculation) { + $this->filterBuilder->setField('price_dynamic_algorithm'); + $this->filterBuilder->setValue($priceRangeCalculation); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + } } } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php new file mode 100644 index 0000000000000..632e1ab9270df --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchCriteriaResolver implements SearchCriteriaResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $builder; + + /** + * @var string + */ + private $searchRequestName; + + /** + * SearchCriteriaResolver constructor. + * @param SearchCriteriaBuilder $builder + * @param string $searchRequestName + */ + public function __construct( + SearchCriteriaBuilder $builder, + string $searchRequestName + ) { + $this->builder = $builder; + $this->searchRequestName = $searchRequestName; + } + + /** + * @inheritdoc + */ + public function resolve() : SearchCriteria + { + $searchCriteria = $this->builder->create(); + $searchCriteria->setRequestName($this->searchRequestName); + + return $searchCriteria; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php new file mode 100644 index 0000000000000..047fa7f71e400 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +interface SearchCriteriaResolverInterface +{ + /** + * Resolve specific attribute. + * + * @return SearchCriteria + */ + public function resolve(): SearchCriteria; +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php new file mode 100644 index 0000000000000..20c237646e524 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\Framework\Data\Collection; +use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; +use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; +use Magento\Framework\Api\Search\SearchResultInterface; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchResultApplier implements SearchResultApplierInterface +{ + /** + * @var Collection + */ + private $collection; + + /** + * @var SearchResultInterface + */ + private $searchResult; + + /** + * @var TemporaryStorageFactory + */ + private $temporaryStorageFactory; + + /** + * @var array + */ + private $orders; + + /** + * @param Collection $collection + * @param SearchResultInterface $searchResult + * @param TemporaryStorageFactory $temporaryStorageFactory + * @param array $orders + */ + public function __construct( + Collection $collection, + SearchResultInterface $searchResult, + TemporaryStorageFactory $temporaryStorageFactory, + array $orders + ) { + $this->collection = $collection; + $this->searchResult = $searchResult; + $this->temporaryStorageFactory = $temporaryStorageFactory; + $this->orders = $orders; + } + + /** + * @inheritdoc + */ + public function apply() + { + $temporaryStorage = $this->temporaryStorageFactory->create(); + $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); + + $this->collection->getSelect()->joinInner( + [ + 'search_result' => $table->getName(), + ], + 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, + [] + ); + + if (isset($this->orders['relevance'])) { + $this->collection->getSelect()->order( + 'search_result.' . TemporaryStorage::FIELD_SCORE . ' ' . $this->orders['relevance'] + ); + } + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php new file mode 100644 index 0000000000000..1b3e2a6bbac71 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplierInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Resolve specific attributes for search criteria. + */ +interface SearchResultApplierInterface +{ + /** + * Apply search results to collection. + * + * @return void + */ + public function apply(); +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php new file mode 100644 index 0000000000000..12b6f81313913 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Resolve total records count. + * + * For Mysql search engine we can't resolve total record count before full load of collection. + */ +class TotalRecordsResolver implements TotalRecordsResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(): ?int + { + return null; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php new file mode 100644 index 0000000000000..190450f9606bc --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolverInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Resolve total records count. + */ +interface TotalRecordsResolverInterface +{ + /** + * Resolve total records. + * + * @return int + */ + public function resolve(): ?int; +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php index b958de91314f4..e625ccbe51fe3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -10,6 +10,7 @@ * Search collection * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -60,7 +61,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -269,6 +269,7 @@ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = tr $sql = $this->_getSearchInOptionSql($query); if ($sql) { + // phpcs:ignore Magento2.SQL.RawQuery $selects[] = "SELECT * FROM ({$sql}) AS inoptionsql"; // inherent unions may be inside } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php new file mode 100644 index 0000000000000..f72e1536f4710 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php @@ -0,0 +1,34 @@ +<?php +declare(strict_types=1); + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Setup; + +use Magento\Eav\Model\Entity\Setup\PropertyMapperAbstract; + +/** + * Class PropertyMapper + * + * @package Magento\CatalogSearch\Model\ResourceModel\Setup + */ +class PropertyMapper extends PropertyMapperAbstract +{ + /** + * Map input attribute properties to storage representation + * + * @param array $input + * @param int $entityTypeId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function map(array $input, $entityTypeId): array + { + return [ + 'search_weight' => $this->_getValue($input, 'search_weight', 1), + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php b/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php index bcd4080b30b14..657c8540d7c68 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php +++ b/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php @@ -44,7 +44,7 @@ public function isCustom(FilterInterface $filter) return $attribute && $filter->getType() === FilterInterface::TYPE_TERM - && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true); + && in_array($attribute->getFrontendInput(), ['select', 'multiselect', 'boolean'], true); } /** diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php new file mode 100644 index 0000000000000..f621bcbf91835 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\Search; + +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\Data\Collection; + +/** + * Search collection provider. + */ +class ItemCollectionProvider implements ItemCollectionProviderInterface +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $factories; + + /** + * ItemCollectionProvider constructor. + * @param EngineResolverInterface $engineResolver + * @param array $factories + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $factories + ) { + $this->engineResolver = $engineResolver; + $this->factories = $factories; + } + + /** + * @inheritdoc + */ + public function getCollection(): Collection + { + if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + } + return $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php new file mode 100644 index 0000000000000..db02d5ac5f519 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProviderInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\Search; + +use Magento\Framework\Data\Collection; + +/** + * Search collection provider. + */ +interface ItemCollectionProviderInterface +{ + /** + * Get collection. + * + * @return Collection + */ + public function getCollection() : Collection; +} diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php index d3c5aa5aaa6f5..8fa9f56d78474 100644 --- a/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php +++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php @@ -25,6 +25,10 @@ class MySQLSearchDeprecationNotification implements \Magento\Framework\Setup\Pat */ private $notifier; + /** + * @param \Magento\Framework\Search\EngineResolverInterface $searchEngineResolver + * @param \Magento\Framework\Notification\NotifierInterface $notifier + */ public function __construct( \Magento\Framework\Search\EngineResolverInterface $searchEngineResolver, \Magento\Framework\Notification\NotifierInterface $notifier diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml new file mode 100644 index 0000000000000..33ffa4fe1b296 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml @@ -0,0 +1,59 @@ +<?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="AssertSearchTermSaveSuccessMessage"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="storeValue" type="string"/> + <argument name="redirectUrl" type="string"/> + <argument name="displayInSuggestedTerm" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.addNewSearchTermButton}}" stepKey="clickAddNewSearchTermButton"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermNewPageLoad"/> + <fillField selector="{{AdminCatalogSearchTermNewSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQueryTextBox"/> + <selectOption selector="{{AdminCatalogSearchTermNewSection.store}}" userInput="{{storeValue}}" stepKey="selectStoreValue"/> + <fillField selector="{{AdminCatalogSearchTermNewSection.redirectUrl}}" userInput="{{redirectUrl}}" stepKey="fillRedirectUrl"/> + <selectOption selector="{{AdminCatalogSearchTermNewSection.displayInSuggestedTerm}}" userInput="{{displayInSuggestedTerm}}" stepKey="selectDisplayInSuggestedTerm"/> + <click selector="{{AdminCatalogSearchTermNewSection.saveSearchButton}}" stepKey="clickSaveSearchButton"/> + <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="You saved the search term." stepKey="seeSaveSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertSearchTermSuccessDeleteMessage"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.nthRow('1')}}" stepKey="checkFirstRow"/> + <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> + <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> + <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> + <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="Total of 1 record(s) were deleted." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertSearchTermNotInGrid"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultToLoad"/> + <see selector="{{AdminCatalogSearchTermIndexSection.emptyRecords}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml new file mode 100644 index 0000000000000..b9ef37cb4effe --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.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="SetMinimalQueryLengthActionGroup"> + <arguments> + <argument name="minLength" type="string" defaultValue="1"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="wait1"/> + <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab"/> + <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" visible="false" stepKey="expandCatalogSearchTab"/> + <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" stepKey="waitTabToCollapse"/> + <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthHint}}" stepKey="seeHint1"/> + <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.maxQueryLengthHint}}" stepKey="seeHint2"/> + <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthInherit}}" stepKey="uncheckSystemValue"/> + <fillField selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" userInput="{{minLength}}" stepKey="setMinQueryLength"/> + <click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml index 387a7547f4daf..067d76821d687 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml @@ -11,15 +11,66 @@ <!-- Quick search the phrase and check if the result page contains correct information --> <actionGroup name="StorefrontCheckQuickSearchActionGroup"> <arguments> - <argument name="phrase"/> + <argument name="phrase" /> </arguments> - <submitForm selector="#search_mini_form" parameterArray="['q' => '{{phrase}}']" stepKey="fillQuickSearch" /> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{phrase}}]" stepKey="fillQuickSearch" /> <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> </actionGroup> + <!-- Quick search the phrase and check if the result page contains correct information, usable with type="string" --> + <actionGroup name="StorefrontCheckQuickSearchStringActionGroup"> + <arguments> + <argument name="phrase" type="string"/> + </arguments> + <fillField stepKey="fillInput" selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{phrase}}"/> + <submitForm selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" parameterArray="[]" stepKey="submitQuickSearch" /> + <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> + <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> + <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> + <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> + </actionGroup> + + <!-- Opens product from QuickSearch and performs assertions--> + <actionGroup name="StorefrontOpenProductFromQuickSearch"> + <arguments> + <argument name="productName" type="string"/> + <argument name="productUrlKey" type="string"/> + </arguments> + <click stepKey="openProduct" selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}"/> + <waitForPageLoad stepKey="waitForProductLoad"/> + <seeInCurrentUrl url="{{productUrlKey}}" stepKey="checkUrl"/> + <see stepKey="checkName" selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}"/> + </actionGroup> + + <!-- Adds product from Quicksearch page and perform assertions--> + <actionGroup name="StorefrontAddToCartFromQuickSearch"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <moveMouseOver stepKey="hoverOverProduct" selector="{{StorefrontQuickSearchResultsSection.productByIndex('1')}}"/> + <click selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}} {{StorefrontQuickSearchResultsSection.addToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> + </actionGroup> + + <actionGroup name="StorefrontQuickSearchCheckProductNameInGrid"> + <arguments> + <argument name="productName" type="string"/> + <argument name="index" type="string"/> + </arguments> + <see selector="{{StorefrontQuickSearchResultsSection.productByIndex(index)}}" userInput="{{productName}}" stepKey="seeProductName"/> + </actionGroup> + + <actionGroup name="StorefrontQuickSearchCheckProductNameNotInGrid"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <dontSee selector="{{StorefrontQuickSearchResultsSection.allResults}}" userInput="{{productName}}" stepKey="dontSeeProductName"/> + </actionGroup> + <!-- Open advanced search page --> <actionGroup name="StorefrontOpenAdvancedSearchActionGroup"> <click selector="{{StorefrontFooterSection.AdvancedSearch}}" stepKey="clickAdvancedSearchLink" /> @@ -48,7 +99,7 @@ <!-- Go to store's advanced catalog search page --> <actionGroup name="GoToStoreViewAdvancedCatalogSearchActionGroup"> <amOnPage url="{{StorefrontCatalogSearchAdvancedFormPage.url}}" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForPageLoad time="90" stepKey="waitForPageLoad"/> </actionGroup> <!-- Storefront advanced catalog search by product name --> @@ -116,4 +167,9 @@ <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="clickSubmit"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <!-- Asserts that search results do not contain any results--> + <actionGroup name="StorefrontCheckSearchIsEmpty"> + <see stepKey="checkEmpty" selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="Your search returned no results"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml new file mode 100644 index 0000000000000..83e4ac50a74e6 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml @@ -0,0 +1,37 @@ +<?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"> + <!--Verify AssertSearchTermNotOnFrontend--> + <actionGroup name="AssertSearchTermNotOnFrontend"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="url_key" type="string"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url('url_key')}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="Your search returned no results." stepKey="seeAssertSearchTermNotOnFrontendNoticeMessage"/> + </actionGroup> + + <actionGroup name="AssertSearchTermOnFrontend"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="redirectUrl" type="string"/> + </arguments> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <waitForPageLoad stepKey="waitForFillField"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <seeInCurrentUrl url="{{redirectUrl}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..df1c3db6e5661 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?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="AdminMenuMarketingSEOAndSearchSearchTerms"> + <data key="pageTitle">Search Terms</data> + <data key="title">Search Terms</data> + <data key="dataUiId">magento-search-search-terms</data> + </entity> + <entity name="AdminMenuReportsMarketingSearchTerms"> + <data key="pageTitle">Search Terms Report</data> + <data key="title">Search Terms</data> + <data key="dataUiId">magento-search-report-search-term</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml new file mode 100644 index 0000000000000..6868456079110 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml @@ -0,0 +1,27 @@ +<?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="SetMinQueryLengthToDefault" type="catalog_search_config_def"> + <requiredEntity type="enable">DefaultMinQueryLength</requiredEntity> + </entity> + <entity name="UncheckMinQueryLengthAndSet" type="catalog_search_config_query_length"> + <requiredEntity type="number">SetMinQueryLengthToOne</requiredEntity> + </entity> + <entity name="DefaultMinQueryLength" type="enable"> + <data key="inherit">true</data> + </entity> + <entity name="DefaultMinQueryLengthDisable" type="enable"> + <data key="inherit">0</data> + </entity> + <entity name="SetMinQueryLengthToOne" type="number"> + <data key="value">1</data> + </entity> + +</entities> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml new file mode 100644 index 0000000000000..6fb254afea347 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml @@ -0,0 +1,14 @@ +<?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="MinMaxQueryLength" type="constant"> + <data key="Hint">This value must be compatible with the corresponding setting in the configured search engine</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml new file mode 100644 index 0000000000000..995b860d107ca --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.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="SimpleTerm" type="searchTerm"> + <data key="search_query" unique="suffix">Query text</data> + <data key="store_id">Default Store View</data> + <data key="redirect" unique="suffix">http://example.com/</data> + <data key="display_in_terms">No</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml new file mode 100644 index 0000000000000..7405377249aa4 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml @@ -0,0 +1,32 @@ +<?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="CatalogSearchConfigDefault" dataType="catalog_search_config_def" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_search_config_def"> + <object key="search" dataType="catalog_search_config_def"> + <object key="fields" dataType="catalog_search_config_def"> + <object key="min_query_length" dataType="enable"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> + <operation name="CatalogSearchConfigQueryLength" dataType="catalog_search_config_query_length" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_search_config_query_length"> + <object key="search" dataType="catalog_search_config_query_length"> + <object key="fields" dataType="catalog_search_config_query_length"> + <object key="min_query_length" dataType="number"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml new file mode 100644 index 0000000000000..bbafff8ad7739 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchTermIndexPage" url="/search/term/index/" area="admin" module="Magento_CatalogSearch"> + <section name="AdminCatalogSearchTermIndexSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml new file mode 100644 index 0000000000000..de7491471741c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchTermNewPage" url="/search/term/new/" area="admin" module="Magento_CatalogSearch"> + <section name="AdminCatalogSearchTermNewSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml new file mode 100644 index 0000000000000..ac316d060f6e9 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchTermIndexSection"> + <element name="addNewSearchTermButton" type="button" selector="//div[@class='page-actions-buttons']/button[@id='add']" timeout="30"/> + <element name="resetFilterButton" type="button" selector="//button[@class='action-default scalable action-reset action-tertiary']" timeout="30"/> + <element name="searchButton" type="button" selector="//button[@class='action-default scalable action-secondary']" timeout="30"/> + <element name="massActions" type="text" selector="//div[@class='admin__grid-massaction-form']//select[@id='search_term_grid_massaction-select']"/> + <element name="submit" type="button" selector="//button[@class='action-default scalable']/span" timeout="30"/> + <element name="searchQuery" type="text" selector="//tr[@class='data-grid-filters']//td/input[@name='search_query']"/> + <element name="nthRow" type="checkbox" selector="//tbody/tr['{{rowNum}}']//input[@name='search']" parameterized="true"/> + <element name="searchTermRowCheckboxBySearchQuery" type="checkbox" selector="//*[normalize-space()='{{var1}}']/preceding-sibling::td//input[@name='search']" parameterized="true" timeout="30"/> + <element name="okButton" type="button" selector="//button[@class='action-primary action-accept']/span" timeout="30"/> + <element name="emptyRecords" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml new file mode 100644 index 0000000000000..5d19198a1b94c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.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="AdminCatalogSearchTermMessagesSection"> + <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div[@data-ui-id='messages-message-success']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml new file mode 100644 index 0000000000000..a7d577a7508c0 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchTermNewSection"> + <element name="searchQuery" type="text" selector="//div[@class='admin__field-control control']/input[@id='query_text']"/> + <element name="store" type="text" selector="//select[@id='store_id']"/> + <element name="redirectUrl" type="text" selector="//div[@class='admin__field-control control']/input[@id='redirect']"/> + <element name="displayInSuggestedTerm" type="select" selector="//select[@name='display_in_terms']"/> + <element name="saveSearchButton" type="button" selector="//button[@id='save']/span[@class='ui-button-text']" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml new file mode 100644 index 0000000000000..605bcabb8a81d --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchConfigurationSection"> + <element name="minQueryLength" type="input" selector="#catalog_search_min_query_length"/> + <element name="minQueryLengthInherit" type="checkbox" selector="#catalog_search_min_query_length_inherit"/> + <element name="minQueryLengthHint" type="text" selector="#row_catalog_search_min_query_length .value span"/> + <element name="maxQueryLengthHint" type="text" selector="#row_catalog_search_max_query_length .value span"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml index 8b35ef3336175..667f08fea6579 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml @@ -15,5 +15,6 @@ <element name="SuccessMsg" type="button" selector="div.message-success"/> <element name="productCount" type="text" selector="#toolbar-amount"/> <element name="message" type="text" selector="div.message div"/> + <element name="searchResults" type="block" selector="#maincontent .column.main"/> </section> </sections> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml new file mode 100644 index 0000000000000..2b425f34f8a5b --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml @@ -0,0 +1,59 @@ +<?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="AdminCreateSearchTermEntityTest"> + <annotations> + <stories value="Search terms"/> + <title value="Create search term test"/> + <description value="Admin should be able to create search term"/> + <testCaseId value="MC-13989"/> + <severity value="CRITICAL"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + </before> + <after> + <!-- Delete created search term --> + <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Delete created product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to the search terms page and create new search term --> + <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="createNewSearchTerm"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> + </actionGroup> + + <!-- Go to storefront --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Assert created search term on storefront --> + <actionGroup ref="AssertSearchTermOnFrontend" stepKey="assertCreatedSearchTermOnFrontend"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml new file mode 100644 index 0000000000000..c72ed424ef307 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml @@ -0,0 +1,59 @@ +<?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="AdminDeleteSearchTermTest"> + <annotations> + <stories value="Search terms"/> + <title value="Delete Search Term and Verify Storefront"/> + <description value="Test log in to SearchTerm and DeleteSearchTerm"/> + <testCaseId value="MC-13988"/> + <severity value="CRITICAL"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Add new search term--> + <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="addNewSearchTerm"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> + </actionGroup> + + <!--Search and delete search term and AssertSearchTermSuccessDeleteMessage--> + <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + </actionGroup> + + <!--Verify deleted search term in grid and AssertSearchTermNotInGrid--> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="verifyDeletedSearchTermNotInGrid"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + </actionGroup> + + <!--Go to storefront and Verify AssertSearchTermNotOnFrontend--> + <actionGroup ref="AssertSearchTermNotOnFrontend" stepKey="verifySearchTermNotOnFrontend"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + <argument name="url_key" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml new file mode 100644 index 0000000000000..bc255020d98b3 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingSearchTermsNavigateMenuTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing search terms navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Search Terms"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14135"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSearchTermsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingSEOAndSearchSearchTerms.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingSEOAndSearchSearchTerms.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml new file mode 100644 index 0000000000000..85cf0e3ba90ed --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsSearchTermsNavigateMenuTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Menu Navigation"/> + <title value="Admin reports search terms navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Search Terms"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14136"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportSearchTermsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingSearchTerms.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingSearchTerms.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml new file mode 100644 index 0000000000000..2fea5c988bf9d --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -0,0 +1,43 @@ +<?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="MinimalQueryLengthForCatalogSearchTest"> + <annotations> + <features value="CatalogSearch"/> + <title value="Minimal query length for catalog search"/> + <description value="Minimal query length for catalog search"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-6325"/> + <useCaseId value="MAGETWO-58764"/> + <group value="CatalogSearch"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="SetMinQueryLengthToDefault" stepKey="setMinimumQueryLengthToDefault"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="SetMinimalQueryLengthActionGroup" stepKey="setMinQueryLength"/> + <comment userInput="Go to Storefront and search for product" stepKey="searchProdUsingMinQueryLength"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="s" stepKey="fillAttribute"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml new file mode 100644 index 0000000000000..19db201e91f40 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml @@ -0,0 +1,629 @@ +<?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="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find products"/> + <description value="Use Quick Search to find a product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14783"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.sku$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="$createSimpleProduct.name$"/> + <argument name="productUrlKey" value="$createSimpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + </test> + <test name="QuickSearchProductByName" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find products via Name"/> + <description value="Use Quick Search to find a product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14791"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <!-- Overwrite search to use name --> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchProductByNameWithSpecialChars" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="Quick Search can find products with names that contain special characters"/> + <description value="Use Quick Search to find a product by name"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14792"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="productWithSpecialCharacters" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <!-- Overwrite search to use name --> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchEmptyResults"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should not get search results on query that doesn't return anything"/> + <description value="Use invalid query to return no products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14793"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="ThisShouldn'tReturnAnything"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmpty"/> + </test> + <test name="QuickSearchWithTwoCharsEmptyResults" extends="QuickSearchEmptyResults"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should not get search results on query that only contains two characters"/> + <description value="Use of 2 character query to return no products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14794"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MC-15827"/> + </skip> + </annotations> + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,2); return ret;" stepKey="getFirstTwoLetters" before="searchStorefront"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{$getFirstTwoLetters}"/> + </actionGroup> + </test> + <test name="QuickSearchProductByNameWithThreeLetters" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find products by their first three letters"/> + <description value="Use Quick Search to find a product using only first three letters"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15034"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,3); return ret;" stepKey="getFirstThreeLetters" before="searchStorefront"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{$getFirstThreeLetters}"/> + </actionGroup> + </test> + <test name="QuickSearchProductBy128CharQuery" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search product with long names, using first 128 letters"/> + <description value="Use Quick Search to find a product with name of 130 length with query of only 128"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14795"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="productWith130CharName" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,128); return ret;" stepKey="get128Letters" before="searchStorefront"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{$get128Letters}"/> + </actionGroup> + </test> + + <test name="QuickSearchTwoProductsWithSameWeight"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="Quick Search should sort products with the same weight appropriately"/> + <description value="Use Quick Search to find a two products with the same weight"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14796"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="productAlphabeticalA" stepKey="product1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAlphabeticalB" stepKey="product2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + + <!-- Create and Assign Attribute to product1--> + <actionGroup ref="goToProductPageViaID" stepKey="goToProduct1"> + <argument name="productId" value="$product1.id$"/> + </actionGroup> + <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct1Attribute"> + <argument name="attributeType" value="Text Field"/> + <argument name="attributeName" value="$product1.name$"/> + <argument name="attributeSetName" value="$product1.name$"/> + <argument name="weight" value="1"/> + <argument name="defaultValue" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet1"> + <argument name="attributeSetName" value="$product1.name$"/> + </actionGroup> + <!--fill in default--> + <actionGroup ref="saveProductForm" stepKey="saveProduct1a"/> + <actionGroup ref="AdminProductPageFillTextAttributeValueByName" stepKey="fillDefault1"> + <argument name="attributeName" value="$product1.name$"/> + <argument name="value" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct1b"/> + <!-- Create and Assign Attribute to product2--> + <actionGroup ref="goToProductPageViaID" stepKey="goToProduct2"> + <argument name="productId" value="$product2.id$"/> + </actionGroup> + <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct2Attribute"> + <argument name="attributeType" value="Text Field"/> + <argument name="attributeName" value="$product2.name$"/> + <argument name="attributeSetName" value="$product2.name$"/> + <argument name="weight" value="1"/> + <argument name="defaultValue" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet2"> + <argument name="attributeSetName" value="$product2.name$"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2a"/> + <!--fill in default--> + <actionGroup ref="AdminProductPageFillTextAttributeValueByName" stepKey="fillDefault2"> + <argument name="attributeName" value="$product2.name$"/> + <argument name="value" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2b"/> + </before> + <after> + <deleteData stepKey="deleteProduct1" createDataKey="product1"/> + <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct1Position"> + <argument name="productName" value="$product1.name$"/> + <argument name="index" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct2Position"> + <argument name="productName" value="$product2.name$"/> + <argument name="index" value="1"/> + </actionGroup> + </test> + <test name="QuickSearchTwoProductsWithDifferentWeight" extends="QuickSearchTwoProductsWithSameWeight"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="Quick Search should sort products with the different weight appropriately"/> + <description value="Use Quick Search to find a two products with the different weight"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14797"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct1Attribute"> + <argument name="attributeType" value="Text Field"/> + <argument name="attributeName" value="$product1.name$"/> + <argument name="attributeSetName" value="$product1.name$"/> + <argument name="weight" value="5"/> + <argument name="defaultValue" value="{{_defaultProduct.name}}"/> + </actionGroup> + </before> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct1Position"> + <argument name="productName" value="$product1.name$"/> + <argument name="index" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct2Position"> + <argument name="productName" value="$product2.name$"/> + <argument name="index" value="2"/> + </actionGroup> + </test> + + <test name="QuickSearchAndAddToCart"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a simple product and add it to cart"/> + <description value="Use Quick Search to find simple Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14784"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createSimpleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartVirtual"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a virtual product and add it to cart"/> + <description value="Use Quick Search to find virtual Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14785"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createVirtualProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createVirtualProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartConfigurable"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a configurable product and add it to cart"/> + <description value="Use Quick Search to find configurable Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14786"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="{{_defaultProduct.name}}"/> + <argument name="productUrlKey" value="{{_defaultProduct.urlKey}}"/> + </actionGroup> + <actionGroup ref="SelectSingleAttributeAndAddToCart" stepKey="addProductToCart"> + <argument name="productName" value="{{_defaultProduct.name}}"/> + <argument name="attributeCode" value="{{colorProductAttribute.default_label}}"/> + <argument name="optionName" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartDownloadable"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a downloadable product and add it to cart"/> + <description value="Use Quick Search to find downloadable Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14787"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="createProduct"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartGrouped"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a grouped product and add it to cart"/> + <description value="Use Quick Search to find grouped Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14788"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1"/> + <createData entity="ApiGroupedProduct" stepKey="createProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createProduct"/> + <requiredEntity createDataKey="simple1"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartBundleDynamic"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a Bundle Dynamic product and add it to cart"/> + <description value="Use Quick Search to find Bundle Dynamic Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14789"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create dynamic product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createProduct"/> + <field key="qty">10</field> + </createData> + <!--Finish bundle creation--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createBundleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productUrlKey" value="$createBundleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="StorefrontAddBundleProductFromProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$createBundleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartBundleFixed"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a Bundle Fixed product and add it to cart"/> + <description value="Use Quick Search to find Bundle Fixed Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14790"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create fixed product--> + <!--Create 2 simple products--> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <!-- Create the bundle product based --> + <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"/> + <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">false</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleOption1_2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + + <!--Finish bundle creation--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <comment userInput="$simpleProduct1.name$" stepKey="asdf"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createBundleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productUrlKey" value="$createBundleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="StorefrontAddBundleProductFromProductToCartWithMultiOption" stepKey="addProductToCart"> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="optionName" value="$createBundleOption1_1.name$"/> + <argument name="value" value="$simpleProduct1.name$"/> + </actionGroup> + </test> + + <test name="QuickSearchConfigurableChildren"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to a configurable product's child products"/> + <description value="Use Quick Search to find a configurable product with enabled/disable children"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14798"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MC-15101"/> + </skip> + </annotations> + <before> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create blank AttributeSet--> + <createData entity="CatalogAttributeSet" stepKey="attributeSet"/> + + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="hiddenDropdownAttributeWithOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Assign attribute to set --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="goToAttributeGridPage" stepKey="goToPage"/> + <actionGroup ref="goToAttributeSetByName" stepKey="goToSet"> + <argument name="name" value="$attributeSet.attribute_set_name$"/> + </actionGroup> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignToAttributeSetAndGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createConfigProductAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="savePage"/> + + <!-- Get the first option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create a simple product,give it the attributeSet and attribute with the first option --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <field key="attribute_set_id">$attributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct1" createDataKey="createConfigChildProduct1"/> + + <!-- Create the configurable product, give it the attributeSet and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <field key="attribute_set_id">$attributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + </before> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createConfigProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="seeProductInGrid"> + <argument name="productName" value="$createConfigProduct.name$"/> + <argument name="index" value="1"/> + </actionGroup> + + <!-- Disable Child Product --> + <actionGroup ref="goToProductPageViaID" stepKey="goToChildProduct"> + <argument name="productId" value="$createConfigChildProduct1.id$"/> + </actionGroup> + <actionGroup ref="toggleProductEnabled" stepKey="disableProduct"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPageAgain"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefrontAgain"> + <argument name="phrase" value="$createConfigProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameNotInGrid" stepKey="dontSeeProductAnymore"> + <argument name="productName" value="$createConfigProduct.name$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php index 2faacea24262c..8d1dbbd6dd6df 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php @@ -20,7 +20,17 @@ public function testResultActionFiltersSetBeforeLoadLayout() $filters = null; $expectedQuery = 'filtersData'; - $view = $this->createPartialMock(\Magento\Framework\App\View::class, ['loadLayout', 'renderLayout']); + $view = $this->createPartialMock( + \Magento\Framework\App\View::class, + ['loadLayout', 'renderLayout', 'getPage', 'getLayout'] + ); + $update = $this->createPartialMock(\Magento\Framework\View\Model\Layout\Merge::class, ['getHandles']); + $update->expects($this->once())->method('getHandles')->will($this->returnValue([])); + $layout = $this->createPartialMock(\Magento\Framework\View\Result\Layout::class, ['getUpdate']); + $layout->expects($this->once())->method('getUpdate')->will($this->returnValue($update)); + $view->expects($this->once())->method('getLayout')->will($this->returnValue($layout)); + $page = $this->createPartialMock(\Magento\Framework\View\Result\Page::class, ['initLayout']); + $view->expects($this->once())->method('getPage')->will($this->returnValue($page)); $view->expects($this->once())->method('loadLayout')->will( $this->returnCallback( function () use (&$filters, $expectedQuery) { @@ -32,15 +42,9 @@ function () use (&$filters, $expectedQuery) { $request = $this->createPartialMock(\Magento\Framework\App\Console\Request::class, ['getQueryValue']); $request->expects($this->once())->method('getQueryValue')->will($this->returnValue($expectedQuery)); - $collection = $this->createPartialMock( - \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, - ['getSize'] - ); - $collection->expects($this->once())->method('getSize')->will($this->returnValue(1)); - $catalogSearchAdvanced = $this->createPartialMock( \Magento\CatalogSearch\Model\Advanced::class, - ['addFilters', '__wakeup', 'getProductCollection'] + ['addFilters', '__wakeup'] ); $catalogSearchAdvanced->expects($this->once())->method('addFilters')->will( $this->returnCallback( @@ -49,8 +53,6 @@ function ($added) use (&$filters) { } ) ); - $catalogSearchAdvanced->expects($this->once())->method('getProductCollection') - ->will($this->returnValue($collection)); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $context = $objectManager->getObject( @@ -189,20 +191,11 @@ public function testNoResultsHandle() $request = $this->createPartialMock(\Magento\Framework\App\Console\Request::class, ['getQueryValue']); $request->expects($this->once())->method('getQueryValue')->will($this->returnValue($expectedQuery)); - $collection = $this->createPartialMock( - \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, - ['getSize'] - ); - $collection->expects($this->once())->method('getSize')->will($this->returnValue(0)); - $catalogSearchAdvanced = $this->createPartialMock( \Magento\CatalogSearch\Model\Advanced::class, ['addFilters', '__wakeup', 'getProductCollection'] ); - $catalogSearchAdvanced->expects($this->once())->method('getProductCollection') - ->will($this->returnValue($collection)); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $context = $objectManager->getObject( \Magento\Framework\App\Action\Context::class, diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php index 85b1b136e78d2..e3cc3e1d18377 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php @@ -20,6 +20,7 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Catalog\Model\Product; use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Event\Manager; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -64,6 +65,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ private $selectBuilderForAttribute; + /** + * @var Manager|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManager; + protected function setUp() { $this->eavConfigMock = $this->createMock(Config::class); @@ -73,12 +79,14 @@ protected function setUp() $this->adapterMock = $this->createMock(AdapterInterface::class); $this->resourceConnectionMock->expects($this->once())->method('getConnection')->willReturn($this->adapterMock); $this->selectBuilderForAttribute = $this->createMock(SelectBuilderForAttribute::class); + $this->eventManager = $this->createMock(Manager::class); $this->model = new DataProvider( $this->eavConfigMock, $this->resourceConnectionMock, $this->scopeResolverMock, $this->sessionMock, - $this->selectBuilderForAttribute + $this->selectBuilderForAttribute, + $this->eventManager ); } @@ -102,6 +110,7 @@ public function testGetDataSetUsesFrontendPriceIndexerTableIfAttributeIsPrice() $selectMock = $this->createMock(Select::class); $this->adapterMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock); + $this->eventManager->expects($this->once())->method('dispatch')->willReturn($selectMock); $tableMock = $this->createMock(Table::class); $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); @@ -129,6 +138,7 @@ public function testGetDataSetUsesFrontendPriceIndexerTableForDecimalAttributes( $selectMock = $this->createMock(Select::class); $this->selectBuilderForAttribute->expects($this->once())->method('build')->willReturn($selectMock); $this->adapterMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock); + $this->eventManager->expects($this->once())->method('dispatch')->willReturn($selectMock); $tableMock = $this->createMock(Table::class); $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php index fc5915bb3cdff..a4f62ce83a1b8 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php @@ -250,6 +250,7 @@ public function testAddFiltersVerifyAddConditionsToRegistry( 'productCollectionFactory' => $productCollectionFactory, 'storeManager' => $this->storeManager, 'currencyFactory' => $currencyFactory, + 'collectionProvider' => null ] ); $instance->addFilters($values); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php index b65a0d6ca47a0..683070c286239 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php @@ -7,7 +7,13 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; /** * Tests Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection @@ -79,6 +85,42 @@ protected function setUp() $productLimitationFactoryMock->method('create') ->willReturn($productLimitationMock); + $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchCriteriaResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($searchCriteriaResolver); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $totalRecordsResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($totalRecordsResolver); + $this->advancedCollection = $this->objectManager->getObject( \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, [ @@ -90,6 +132,10 @@ protected function setUp() 'temporaryStorageFactory' => $this->temporaryStorageFactory, 'search' => $this->search, 'productLimitationFactory' => $productLimitationFactoryMock, + 'collectionProvider' => null, + 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, + 'searchResultApplierFactory' => $searchResultApplierFactory, + 'totalRecordsResolverFactory' => $totalRecordsResolverFactory ] ); } @@ -117,18 +163,8 @@ public function testLike() ->willReturn($this->filterBuilder); $filter = $this->createMock(\Magento\Framework\Api\Filter::class); - $this->filterBuilder->expects($this->once())->method('create')->willReturn($filter); - - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->with('advanced_search_container'); - - $tempTable = $this->createMock(\Magento\Framework\DB\Ddl\Table::class); - $temporaryStorage = $this->createMock(\Magento\Framework\Search\Adapter\Mysql\TemporaryStorage::class); - $temporaryStorage->expects($this->once())->method('storeApiDocuments')->willReturn($tempTable); - $this->temporaryStorageFactory->expects($this->once())->method('create')->willReturn($temporaryStorage); + $this->filterBuilder->expects($this->any())->method('create')->willReturn($filter); + $searchResult = $this->createMock(\Magento\Framework\Api\Search\SearchResultInterface::class); $this->search->expects($this->once())->method('search')->willReturn($searchResult); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php index a3b1d2fd0f2b6..9170b81dc3182 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -5,6 +5,12 @@ */ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -97,6 +103,41 @@ protected function setUp() $temporaryStorageFactory->expects($this->any()) ->method('create') ->willReturn($this->temporaryStorage); + $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchCriteriaResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($searchCriteriaResolver); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $totalRecordsResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($totalRecordsResolver); $this->model = $this->objectManager->getObject( \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection::class, @@ -106,6 +147,9 @@ protected function setUp() 'scopeConfig' => $this->scopeConfig, 'temporaryStorageFactory' => $temporaryStorageFactory, 'productLimitationFactory' => $productLimitationFactoryMock, + 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, + 'searchResultApplierFactory' => $searchResultApplierFactory, + 'totalRecordsResolverFactory' => $totalRecordsResolverFactory, ] ); @@ -124,37 +168,10 @@ protected function tearDown() $reflectionProperty->setValue(null); } - /** - * @expectedException \Exception - * @expectedExceptionCode 333 - * @expectedExceptionMessage setRequestName - */ - public function testGetFacetedDataWithException() - { - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->withConsecutive(['catalog_view_container']) - ->willThrowException(new \Exception('setRequestName', 333)); - $this->model->getFacetedData('field'); - } - public function testGetFacetedDataWithEmptyAggregations() { - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->withConsecutive(['catalog_view_container']); $searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class) ->getMockForAbstractClass(); - $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) - ->setMethods(['getName']) - ->getMock(); - $this->temporaryStorage->expects($this->once()) - ->method('storeApiDocuments') - ->willReturn($table); $this->search->expects($this->once()) ->method('search') ->willReturn($searchResult); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php new file mode 100644 index 0000000000000..5c917b360f147 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php @@ -0,0 +1,65 @@ +<?php +declare(strict_types=1); + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Setup; + +use Magento\CatalogSearch\Model\ResourceModel\Setup\PropertyMapper; +use PHPUnit\Framework\TestCase; + +/** + * Class PropertyMapperTest + * + * @package Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Setup + */ +class PropertyMapperTest extends TestCase +{ + /** + * @var PropertyMapper + */ + private $propertyMapper; + + /** + * @return void + */ + protected function setUp(): void + { + $this->propertyMapper = new PropertyMapper(); + } + + /** + * @return array + */ + public function caseProvider(): array + { + return [ + [ + ['search_weight' => 9, 'something_other' => '3'], + ['search_weight' => 9] + ], + [ + ['something' => 3], + ['search_weight' => 1] + ] + ]; + } + + /** + * @dataProvider caseProvider + * + * @test + * + * @param array $input + * @param array $result + * @return void + */ + public function testMapCorrectlyMapsValue(array $input, array $result): void + { + //Second parameter doesn't matter as it is not used + $this->assertSame($result, $this->propertyMapper->map($input, 4)); + } +} diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml index b8f2863139e9b..c358062b88a41 100644 --- a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml @@ -19,13 +19,15 @@ <field id="engine" canRestore="1"> <backend_model>Magento\CatalogSearch\Model\Adminhtml\System\Config\Backend\Engine</backend_model> </field> - <field id="min_query_length" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="min_query_length" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minimal Query Length</label> <validate>validate-digits</validate> + <comment>This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.</comment> </field> - <field id="max_query_length" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="max_query_length" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Maximum Query Length</label> <validate>validate-digits</validate> + <comment>This value must be compatible with the corresponding setting in the configured search engine.</comment> </field> <field id="max_count_cacheable_search_terms" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Number of top search results to cache</label> diff --git a/app/code/Magento/CatalogSearch/etc/config.xml b/app/code/Magento/CatalogSearch/etc/config.xml index 66b79226c9f34..7ea15c6caa590 100644 --- a/app/code/Magento/CatalogSearch/etc/config.xml +++ b/app/code/Magento/CatalogSearch/etc/config.xml @@ -13,7 +13,7 @@ </seo> <search> <engine>mysql</engine> - <min_query_length>1</min_query_length> + <min_query_length>3</min_query_length> <max_query_length>128</max_query_length> <max_count_cacheable_search_terms>100</max_count_cacheable_search_terms> <autocomplete_limit>8</autocomplete_limit> diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index cc07384d4c525..7359bd6b454b9 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -14,6 +14,10 @@ <preference for="Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface" type="Magento\CatalogSearch\Model\Search\FilterMapper\FilterContext"/> <preference for="Magento\CatalogSearch\Model\Indexer\IndexSwitcherInterface" type="Magento\CatalogSearch\Model\Indexer\IndexSwitcherProxy"/> <preference for="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerInterface" type="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerComposite"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver"/> + <preference for="Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface" type="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"/> <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> <arguments> <argument name="handlers" xsi:type="array"> @@ -176,8 +180,13 @@ <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</argument> </arguments> </virtualType> - <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" - type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> + + <virtualType name="Magento\CatalogSearch\Model\Advanced\ItemCollectionProvider" type="Magento\Catalog\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> + </arguments> + </virtualType> + <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> <arguments> <argument name="instanceName" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection</argument> </arguments> @@ -185,6 +194,21 @@ <type name="Magento\CatalogSearch\Model\Advanced"> <arguments> <argument name="productCollectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> + <argument name="collectionProvider" xsi:type="object">Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface</argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> </arguments> </type> <virtualType name="Magento\CatalogSearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> @@ -340,4 +364,11 @@ <type name="Magento\Config\Model\Config"> <plugin name="config_enable_eav_indexer" type="Magento\CatalogSearch\Plugin\EnableEavIndexer" /> </type> + <type name="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite"> + <arguments> + <argument name="propertyMappers" xsi:type="array"> + <item name="catalog_search" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Setup\PropertyMapper</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogSearch/i18n/en_US.csv b/app/code/Magento/CatalogSearch/i18n/en_US.csv index ba97dc9de1d31..f25d1a589d455 100644 --- a/app/code/Magento/CatalogSearch/i18n/en_US.csv +++ b/app/code/Magento/CatalogSearch/i18n/en_US.csv @@ -38,3 +38,5 @@ name,name "Maximum Query Length","Maximum Query Length" "Rebuild Catalog product fulltext search index","Rebuild Catalog product fulltext search index" "Please enter a valid price range.","Please enter a valid price range." +"This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.","This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact." +"This value must be compatible with the corresponding setting in the configured search engine.","This value must be compatible with the corresponding setting in the configured search engine." \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml index 83808df5b95e4..3f616ab791dfe 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml @@ -11,6 +11,8 @@ /** * @var $block \Magento\CatalogSearch\Block\Advanced\Result */ +/** this changes need for valid apply filters and configuration before search process is started */ +$productList = $block->getProductListHtml(); ?> <?php if ($results = $block->getResultCount()): ?> <div class="search found"> @@ -49,6 +51,6 @@ </div> <?php endif; ?> <?php if ($block->getResultCount()): ?> - <div class="search results"><?= $block->getProductListHtml() ?></div> + <div class="search results"><?= /* @escapeNotVerified */ $productList ?></div> <?php endif; ?> <?php $block->getSearchCriterias(); ?> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php new file mode 100644 index 0000000000000..f5851cf1e11b1 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php @@ -0,0 +1,127 @@ +<?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\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Save/Delete UrlRewrites by Product ID's and visibility + */ +class AdaptUrlRewritesToVisibilityAttribute +{ + /** + * @var CollectionFactory + */ + private $productCollectionFactory; + + /** + * @var ProductUrlRewriteGenerator + */ + private $urlRewriteGenerator; + + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var ProductUrlPathGenerator + */ + private $urlPathGenerator; + + /** + * @param CollectionFactory $collectionFactory + * @param ProductUrlRewriteGenerator $urlRewriteGenerator + * @param UrlPersistInterface $urlPersist + * @param ProductUrlPathGenerator|null $urlPathGenerator + */ + public function __construct( + CollectionFactory $collectionFactory, + ProductUrlRewriteGenerator $urlRewriteGenerator, + UrlPersistInterface $urlPersist, + ProductUrlPathGenerator $urlPathGenerator + ) { + $this->productCollectionFactory = $collectionFactory; + $this->urlRewriteGenerator = $urlRewriteGenerator; + $this->urlPersist = $urlPersist; + $this->urlPathGenerator = $urlPathGenerator; + } + + /** + * Process Url Rewrites according to the products visibility attribute + * + * @param array $productIds + * @param int $visibility + * @throws UrlAlreadyExistsException + */ + public function execute(array $productIds, int $visibility): void + { + $products = $this->getProductsByIds($productIds); + + /** @var Product $product */ + foreach ($products as $product) { + if ($visibility == Visibility::VISIBILITY_NOT_VISIBLE) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + ] + ); + } elseif ($visibility !== Visibility::VISIBILITY_NOT_VISIBLE) { + $product->setVisibility($visibility); + $productUrlPath = $this->urlPathGenerator->getUrlPath($product); + $productUrlRewrite = $this->urlRewriteGenerator->generate($product); + $product->unsUrlPath(); + $product->setUrlPath($productUrlPath); + + try { + $this->urlPersist->replace($productUrlRewrite); + } catch (UrlAlreadyExistsException $e) { + throw new UrlAlreadyExistsException( + __( + 'Can not change the visibility of the product with SKU equals "%1". ' + . 'URL key "%2" for specified store already exists.', + $product->getSku(), + $product->getUrlKey() + ), + $e, + $e->getCode(), + $e->getUrls() + ); + } + } + } + } + + /** + * Get Product Models by Id's + * + * @param array $productIds + * @return array + */ + private function getProductsByIds(array $productIds): array + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect(ProductInterface::VISIBILITY); + $productCollection->addAttributeToSelect('url_key'); + $productCollection->addFieldToFilter( + 'entity_id', + ['in' => array_unique($productIds)] + ); + + return $productCollection->getItems(); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php index 685a9d2828741..311cc6de76114 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php @@ -8,6 +8,9 @@ use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\UrlRewrite\Model\Storage\DbStorage; +/** + * Product Resource Class + */ class Product extends AbstractDb { /** @@ -38,6 +41,8 @@ protected function _construct() } /** + * Save multiple data + * * @param array $insertData * @return int */ @@ -70,7 +75,10 @@ public function removeMultiple(array $removeData) } /** - * Removes multiple entities from url_rewrite table using entities from catalog_url_rewrite_product_category + * Removes multiple data by filter + * + * Removes multiple entities from url_rewrite table + * using entities from catalog_url_rewrite_product_category * Example: $filter = ['category_id' => [1, 2, 3], 'product_id' => [1, 2, 3]] * * @param array $filter @@ -78,10 +86,7 @@ public function removeMultiple(array $removeData) */ public function removeMultipleByProductCategory(array $filter) { - return $this->getConnection()->delete( - $this->getTable(self::TABLE_NAME), - ['url_rewrite_id in (?)' => $this->prepareSelect($filter)] - ); + return $this->getConnection()->deleteFromSelect($this->prepareSelect($filter), self::TABLE_NAME); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 022a78be00197..7b60c85049767 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -135,6 +135,7 @@ class AfterImportDataObserver implements ObserverInterface 'url_path', 'name', 'visibility', + 'save_rewrites_history' ]; /** @@ -199,6 +200,7 @@ public function __construct( /** * Action after data import. + * * Save new url rewrites and remove old if exist. * * @param Observer $observer @@ -261,12 +263,15 @@ protected function _populateForUrlGeneration($rowData) if ($this->isGlobalScope($product->getStoreId())) { $this->populateGlobalProduct($product); } else { + $this->storesCache[$product->getStoreId()] = true; $this->addProductToImport($product, $product->getStoreId()); } return $this; } /** + * Add store id to product data. + * * @param \Magento\Catalog\Model\Product $product * @param array $rowData * @return void @@ -436,6 +441,8 @@ protected function currentUrlRewritesRegenerate() } /** + * Generate url-rewrite for outogenerated url-rewirte. + * * @param UrlRewrite $url * @param Category $category * @return array @@ -470,6 +477,8 @@ protected function generateForAutogenerated($url, $category) } /** + * Generate url-rewrite for custom url-rewirte. + * * @param UrlRewrite $url * @param Category $category * @return array @@ -503,6 +512,8 @@ protected function generateForCustom($url, $category) } /** + * Retrieve category from url metadata. + * * @param UrlRewrite $url * @return Category|null|bool */ @@ -517,6 +528,8 @@ protected function retrieveCategoryFromMetadata($url) } /** + * Check, category suited for url-rewrite generation. + * * @param \Magento\Catalog\Model\Category $category * @param int $storeId * @return bool diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php index 5d7e323e8b2d8..3cfd49b1d210a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php @@ -100,16 +100,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) } $mapsGenerated = false; - if ($category->dataHasChangedFor('url_key') - || $category->dataHasChangedFor('is_anchor') - || !empty($category->getChangedProductIds()) - ) { + if ($this->isCategoryHasChanged($category)) { if ($category->dataHasChangedFor('url_key')) { $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); } - $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); - $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + if ($this->isChangedOnlyProduct($category)) { + $productUrlRewriteResult = + $this->urlRewriteHandler->updateProductUrlRewritesForChangedProduct($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } else { + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } $mapsGenerated = true; } @@ -119,6 +122,38 @@ public function execute(\Magento\Framework\Event\Observer $observer) } } + /** + * Check is category changed changed. + * + * @param Category $category + * @return bool + */ + private function isCategoryHasChanged(Category $category): bool + { + if ($category->dataHasChangedFor('url_key') + || $category->dataHasChangedFor('is_anchor') + || !empty($category->getChangedProductIds())) { + return true; + } + return false; + } + + /** + * Check is only product changed. + * + * @param Category $category + * @return bool + */ + private function isChangedOnlyProduct(Category $category): bool + { + if (!empty($category->getChangedProductIds()) + && !$category->dataHasChangedFor('is_anchor') + && !$category->dataHasChangedFor('url_key')) { + return true; + } + return false; + } + /** * In case store_id is not set for category then we can assume that it was passed through product import. * Store group must have only one root category, so receiving category's path and checking if one of it parts diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php new file mode 100644 index 0000000000000..2337bb3646bd7 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogUrlRewrite\Model\Products\AdaptUrlRewritesToVisibilityAttribute; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; + +/** + * Consider URL rewrites on change product visibility via mass action + */ +class ProcessUrlRewriteOnChangeProductVisibilityObserver implements ObserverInterface +{ + /** + * @var AdaptUrlRewritesToVisibilityAttribute + */ + private $adaptUrlRewritesToVisibility; + + /** + * @param AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility + */ + public function __construct(AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility) + { + $this->adaptUrlRewritesToVisibility = $adaptUrlRewritesToVisibility; + } + + /** + * Generate urls for UrlRewrites and save it in storage + * + * @param Observer $observer + * @return void + * @throws UrlAlreadyExistsException + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + $attrData = $event->getAttributesData(); + $productIds = $event->getProductIds(); + $visibility = $attrData[ProductInterface::VISIBILITY] ?? 0; + + if (!$visibility || !$productIds) { + return; + } + + $this->adaptUrlRewritesToVisibility->execute($productIds, (int)$visibility); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php index fc2056e83ec70..44b47faf3d4b8 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php @@ -13,7 +13,12 @@ use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Store\Api\StoreWebsiteRelationInterface; +use Magento\Framework\App\ObjectManager; +/** + * Observer to assign the products to website + */ class ProductToWebsiteChangeObserver implements ObserverInterface { /** @@ -36,22 +41,31 @@ class ProductToWebsiteChangeObserver implements ObserverInterface */ 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 + 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); } /** @@ -69,12 +83,21 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) ); - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ]); - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); + if (!empty($this->productUrlRewriteGenerator->generate($product))) { + 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/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index c4ec0bb3a74b2..b4a35f323e1bc 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -24,6 +24,8 @@ use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** + * Class for management url rewrites. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UrlRewriteHandler @@ -156,6 +158,30 @@ public function generateProductUrlRewrites(Category $category): array } /** + * Update product url rewrites for changed product. + * + * @param Category $category + * @return array + */ + public function updateProductUrlRewritesForChangedProduct(Category $category): array + { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $this->isSkippedProduct[$category->getEntityId()] = []; + $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); + $storeIds = $this->getCategoryStoreIds($category); + + if ($category->getChangedProductIds()) { + foreach ($storeIds as $storeId) { + $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); + } + } + + return $mergeDataProvider->getData(); + } + + /** + * Delete category rewrites for children. + * * @param Category $category * @return void */ @@ -184,6 +210,8 @@ public function deleteCategoryRewritesForChildren(Category $category) } /** + * Get category products url rewrites. + * * @param Category $category * @param int $storeId * @param bool $saveRewriteHistory diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml new file mode 100644 index 0000000000000..30a4290d882fb --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUrlForProductRewrittenCorrectlyTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <title value="Check that URL for product rewritten correctly"/> + <description value="Check that URL for product rewritten correctly"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97224"/> + <useCaseId value="MAGETWO-64191"/> + <group value="CatalogUrlRewrite"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create product--> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="amOnEditPage"/> + <waitForPageLoad stepKey="waitForEditPage"/> + + <!--Switch to Default Store view--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> + <argument name="storeViewName" value="Default Store View"/> + </actionGroup> + <waitForPageLoad stepKey="waitForStoreViewLoad"/> + + <!--Set use default url--> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickOnSearchEngineOptimization"/> + <waitForElementVisible selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="waitForUseDefaultUrlCheckbox"/> + <click selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="clickUseDefaultUrlCheckbox"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$createProduct.sku$$-new" stepKey="changeUrlKey"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Select product and go toUpdate Attribute page--> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> + <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> + <actionGroup ref="filterProductGridByName" stepKey="filterBundleProductOptionsDownToName"> + <argument name="product" value="ApiSimpleProduct"/> + </actionGroup> + <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="ClickOnSelectAllCheckBoxChangingView"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickBulkUpdate"/> + <waitForPageLoad stepKey="waitForUpdateAttributesPageLoad"/> + <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeInUrlAttributeUpdatePage"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.website}}" stepKey="clickWebsiteTab"/> + <waitForAjaxLoad stepKey="waitForLoadWebSiteTab"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.addProductToWebsite}}" stepKey="checkAddProductToWebsiteCheckbox"/> + <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> + + <!--Got to Store front product page and check url--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="navigateToSimpleProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="seeProductNewUrl"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php index fbc620a6d741a..294cf8562906d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/CurrentUrlRewritesRegeneratorTest.php @@ -252,7 +252,7 @@ protected function getCurrentRewritesMocks($currentRewrites) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) - ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) + ->method('get' . str_replace('_', '', ucwords($key, '_'))) ->will($this->returnValue($value)); } $rewrites[] = $url; diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php index 4855478b8488a..c431743fc0b51 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/CurrentUrlRewritesRegeneratorTest.php @@ -294,7 +294,7 @@ protected function getCurrentRewritesMocks($currentRewrites) ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) - ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) + ->method('get' . str_replace('_', '', ucwords($key, '_'))) ->will($this->returnValue($value)); } $rewrites[] = $url; diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index fd9ab10537f1c..3984d949332d3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -694,7 +694,7 @@ protected function currentUrlRewritesRegeneratorGetCurrentRewritesMocks($current ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) - ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) + ->method('get' . str_replace('_', '', ucwords($key, '_'))) ->will($this->returnValue($value)); } $rewrites[] = $url; diff --git a/app/code/Magento/CatalogUrlRewrite/etc/events.xml b/app/code/Magento/CatalogUrlRewrite/etc/events.xml index cc558fe81f16d..728442acf7a44 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/events.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/events.xml @@ -27,6 +27,9 @@ <event name="catalog_product_save_after"> <observer name="process_url_rewrite_saving" instance="Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver"/> </event> + <event name="catalog_product_attribute_update_before"> + <observer name="process_url_rewrite_on_change_product_visibility" instance="Magento\CatalogUrlRewrite\Observer\ProcessUrlRewriteOnChangeProductVisibilityObserver"/> + </event> <event name="catalog_category_save_before"> <observer name="category_url_path_autogeneration" instance="Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver"/> </event> diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 2b95de24d0201..9e47830debfc4 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -6,10 +6,13 @@ namespace Magento\CatalogWidget\Block\Product; +use Magento\Catalog\Model\Product; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ActionInterface; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\LayoutFactory; use Magento\Widget\Block\BlockInterface; use Magento\Framework\Url\EncoderInterface; @@ -96,11 +99,21 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem */ private $json; + /** + * @var LayoutFactory + */ + private $layoutFactory; + /** * @var \Magento\Framework\Url\EncoderInterface|null */ private $urlEncoder; + /** + * @var \Magento\Framework\View\Element\RendererList + */ + private $rendererListBlock; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory @@ -111,7 +124,10 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem * @param \Magento\Widget\Helper\Conditions $conditionsHelper * @param array $data * @param Json|null $json + * @param LayoutFactory|null $layoutFactory * @param \Magento\Framework\Url\EncoderInterface|null $urlEncoder + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Catalog\Block\Product\Context $context, @@ -123,6 +139,7 @@ public function __construct( \Magento\Widget\Helper\Conditions $conditionsHelper, array $data = [], Json $json = null, + LayoutFactory $layoutFactory = null, EncoderInterface $urlEncoder = null ) { $this->productCollectionFactory = $productCollectionFactory; @@ -132,6 +149,7 @@ public function __construct( $this->rule = $rule; $this->conditionsHelper = $conditionsHelper; $this->json = $json ?: ObjectManager::getInstance()->get(Json::class); + $this->layoutFactory = $layoutFactory ?: ObjectManager::getInstance()->get(LayoutFactory::class); $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance()->get(EncoderInterface::class); parent::__construct( $context, @@ -179,6 +197,7 @@ public function getCacheKeyInfo() $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP), (int) $this->getRequest()->getParam($this->getData('page_var_name'), 1), $this->getProductsPerPage(), + $this->getProductsCount(), $conditions, $this->json->serialize($this->getRequest()->getParams()), $this->getTemplate(), @@ -228,6 +247,41 @@ public function getProductPriceHtml( return $price; } + /** + * @inheritdoc + */ + protected function getDetailsRendererList() + { + if (empty($this->rendererListBlock)) { + /** @var $layout \Magento\Framework\View\LayoutInterface */ + $layout = $this->layoutFactory->create(['cacheable' => false]); + $layout->getUpdate()->addHandle('catalog_widget_product_list')->load(); + $layout->generateXml(); + $layout->generateElements(); + + $this->rendererListBlock = $layout->getBlock('category.product.type.widget.details.renderers'); + } + return $this->rendererListBlock; + } + + /** + * Get post parameters. + * + * @param Product $product + * @return array + */ + public function getAddToCartPostParams(Product $product) + { + $url = $this->getAddToCartUrl($product); + return [ + 'action' => $url, + 'data' => [ + 'product' => $product->getEntityId(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($url), + ] + ]; + } + /** * @inheritdoc */ @@ -247,10 +301,16 @@ public function createCollection() { /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $collection = $this->productCollectionFactory->create(); + + if ($this->getData('store_id') !== null) { + $collection->setStoreId($this->getData('store_id')); + } + $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); $collection = $this->_addProductAttributesAndPrices($collection) ->addStoreFilter() + ->addAttributeToSort('created_at', 'desc') ->setPageSize($this->getPageSize()) ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1)); diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml index 2957cfdfe6f43..855d325c9850c 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml @@ -21,5 +21,6 @@ <element name="conditionIs" type="button" selector="//*[@id='conditions__1--1__attribute']/following-sibling::span[1]"/> <element name="conditionOperator" type="button" selector="#conditions__1--{{arg3}}__operator" parameterized="true"/> <element name="checkElementStorefrontByPrice" type="button" selector="//*[@class='product-items widget-product-grid']//*[contains(text(),'${{arg4}}.00')]" parameterized="true"/> + <element name="checkElementStorefrontByName" type="button" selector="//*[@class='product-items widget-product-grid']//*[@class='product-item'][{{productPosition}}]//a[contains(text(), '{{productName}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml new file mode 100644 index 0000000000000..11586207c4d8e --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml @@ -0,0 +1,88 @@ +<?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="CatalogProductListWidgetOrderTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="MC-5905: Wrong sorting on Products component"/> + <title value="Checking order of products in the 'catalog Products List' widget"/> + <description value="Check that products are ordered with recently added products first"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13794"/> + <group value="CatalogWidget"/> + <group value="WYSIWYGDisabled"/> + <skip> + <issueId value="MC-13923"/> + </skip> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">20</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">30</field> + </createData> + <createData entity="_defaultCmsPage" stepKey="createPreReqPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> + </before> + <!--Open created cms page--> + <comment userInput="Open created cms page" stepKey="commentOpenCreatedCmsPage"/> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage1"> + <argument name="CMSPage" value="$$createPreReqPage$$"/> + </actionGroup> + <!--Add widget to cms page--> + <comment userInput="Add widget to cms page" stepKey="commentAddWidgetToCmsPage"/> + <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> + <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> + <click selector="{{WidgetSection.PreCreateCategory('$$simplecategory.name$$')}}" stepKey="selectCategory" /> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + <!--Save cms page and go to Storefront--> + <comment userInput="Save cms page and go to Storefront" stepKey="commentSaveCmsPageAndGoToStorefront"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <amOnPage url="$$createPreReqPage.identifier$$" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="waitForPageLoad3" /> + <!--Check order of products: recently added first--> + <comment userInput="Check order of products: recently added first" stepKey="commentCheckOrderOfProductsRecentlyAddedFirst"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$$createThirdProduct.name$$')}}" stepKey="seeElementByName1"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$$createSecondProduct.name$$')}}" stepKey="seeElementByName2"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$$createFirstProduct.name$$')}}" stepKey="seeElementByName3"/> + <after> + <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php index 5de8b9d9632fc..a789753795724 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php @@ -167,6 +167,7 @@ public function testGetCacheKeyInfo() 'context_group', 1, 5, + 10, 'some_serialized_conditions', json_encode('request_params'), 'test_template', @@ -274,6 +275,7 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP 'addAttributeToSelect', 'addUrlRewrite', 'addStoreFilter', + 'addAttributeToSort', 'setPageSize', 'setCurPage', 'distinct' @@ -288,6 +290,7 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP $collection->expects($this->once())->method('addAttributeToSelect')->willReturnSelf(); $collection->expects($this->once())->method('addUrlRewrite')->willReturnSelf(); $collection->expects($this->once())->method('addStoreFilter')->willReturnSelf(); + $collection->expects($this->once())->method('addAttributeToSort')->with('created_at', 'desc')->willReturnSelf(); $collection->expects($this->once())->method('setPageSize')->with($expectedPageSize)->willReturnSelf(); $collection->expects($this->once())->method('setCurPage')->willReturnSelf(); $collection->expects($this->once())->method('distinct')->willReturnSelf(); diff --git a/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 0000000000000..db44d8b62dc1a --- /dev/null +++ b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,17 @@ +<!-- + ~ Copyright © Magento, Inc. All rights reserved. + ~ See COPYING.txt for license details. + --> + +<!-- + ~ Copyright © Magento, Inc. All rights reserved. + ~ See COPYING.txt for license details. + --> + +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.widget.details.renderers"> + <block class="Magento\Framework\View\Element\Template" name="category.product.type.details.renderers.default" as="default"/> + </block> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index 6789ace243b84..29efe8a8c1c6a 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\Framework\App\Action\Action; // @codingStandardsIgnoreFile /** @var \Magento\CatalogWidget\Block\Product\ProductsList $block */ ?> <?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())): ?> -<?php + <?php $type = 'widget-product-grid'; $mode = 'grid'; @@ -20,14 +21,14 @@ $showWishlist = true; $showCompare = true; $showCart = true; - $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::DEFAULT_VIEW; + $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW; $description = false; -?> + ?> <div class="block widget block-products-list <?= /* @noEscape */ $mode ?>"> <?php if ($block->getTitle()): ?> - <div class="block-title"> - <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> - </div> + <div class="block-title"> + <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> + </div> <?php endif ?> <div class="block-content"> <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> @@ -48,57 +49,57 @@ <?= $block->escapeHtml($_item->getName()) ?> </a> </strong> - <?php - echo $block->getProductPriceHtml($_item, $type); - ?> - <?php if ($templateType): ?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> + <?= $block->getProductPriceHtml($_item, $type) ?> + + <?= $block->getProductDetailsHtml($_item) ?> + <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if (!$_item->getTypeInstance()->isPossibleBuyFromList($_item)): ?> - <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> + <div class="product-item-inner"> + <div class="product-item-actions"> + <?php if ($showCart): ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()): ?> + <?php $postParams = $block->getAddToCartPostParams($_item); ?> + <form data-role="tocart-form" data-product-sku="<?= $block->escapeHtml($_item->getSku()) ?>" action="<?= /* @NoEscape */ $postParams['action'] ?>" method="post"> + <input type="hidden" name="product" value="<?= /* @escapeNotVerified */ $postParams['data']['product'] ?>"> + <input type="hidden" name="<?= /* @escapeNotVerified */ Action::PARAM_NAME_URL_ENCODED ?>" value="<?= /* @escapeNotVerified */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <?= $block->getBlockHtml('formkey') ?> + <button type="submit" + title="<?= $block->escapeHtml(__('Add to Cart')) ?>" + class="action tocart primary"> + <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + </button> + </form> <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" data-post='<?= /* @noEscape */ $postData ?>' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> + <?php if ($_item->getIsSalable()): ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else: ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + </div> + <?php endif; ?> + <?php if ($showWishlist || $showCompare): ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> + <?php endif; ?> + <?php if ($block->getAddToCompareUrl() && $showCompare): ?> + <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> + <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> - <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> - <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> + </div> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 92ba6bf2bbbb1..c5e309df3cad6 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -82,11 +82,14 @@ public function getConfig() 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), - 'maxItemsToDisplay' => $this->getMaxItemsToDisplay() + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), + 'storeId' => $this->_storeManager->getStore()->getId() ]; } /** + * Get serialized config + * * @return string * @since 100.2.0 */ @@ -96,6 +99,8 @@ public function getSerializedConfig() } /** + * Get image html template + * * @return string */ public function getImageHtmlTemplate() @@ -130,6 +135,7 @@ public function getShoppingCartUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getUpdateItemQtyUrl() { @@ -141,6 +147,7 @@ public function getUpdateItemQtyUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getRemoveItemUrl() { @@ -210,6 +217,7 @@ private function getMiniCartMaxItemsCount() /** * Returns maximum cart items to display + * * This setting regulates how many items will be displayed in minicart * * @return int diff --git a/app/code/Magento/Checkout/Block/Cart/Totals.php b/app/code/Magento/Checkout/Block/Cart/Totals.php index 375c564f29059..7ac5c1c149cc6 100644 --- a/app/code/Magento/Checkout/Block/Cart/Totals.php +++ b/app/code/Magento/Checkout/Block/Cart/Totals.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Checkout\Block\Cart; use Magento\Framework\View\Element\BlockInterface; use Magento\Checkout\Block\Checkout\LayoutProcessorInterface; /** + * Totals cart block. + * * @api */ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart @@ -62,6 +65,8 @@ public function __construct( } /** + * Retrieve encoded js layout. + * * @return string */ public function getJsLayout() @@ -74,6 +79,8 @@ public function getJsLayout() } /** + * Retrieve totals from cache. + * * @return array */ public function getTotals() @@ -85,6 +92,8 @@ public function getTotals() } /** + * Set totals to cache. + * * @param array $value * @return $this * @codeCoverageIgnore @@ -96,6 +105,8 @@ public function setTotals($value) } /** + * Create totals block and set totals. + * * @param string $code * @return BlockInterface */ @@ -121,6 +132,8 @@ protected function _getTotalRenderer($code) } /** + * Get totals html. + * * @param mixed $total * @param int|null $area * @param int $colspan @@ -177,7 +190,7 @@ public function needDisplayBaseGrandtotal() } /** - * Get formated in base currency base grand total value + * Get formatted in base currency base grand total value * * @return string */ diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index de996bed02439..5dedf2c7e7eba 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -6,10 +6,18 @@ namespace Magento\Checkout\Block\Checkout; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Customer\Model\Session; use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +/** + * Fields attribute merger. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class AttributeMerger { /** @@ -46,6 +54,7 @@ class AttributeMerger 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'email2', 'length' => 'validate-length', @@ -67,7 +76,7 @@ class AttributeMerger private $customerRepository; /** - * @var \Magento\Customer\Api\Data\CustomerInterface + * @var CustomerInterface */ private $customer; @@ -269,6 +278,7 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi for ($lineIndex = 0; $lineIndex < (int)$attributeConfig['size']; $lineIndex++) { $isFirstLine = $lineIndex === 0; $line = [ + 'label' => __("%1: Line %2", $attributeConfig['label'], $lineIndex + 1), 'component' => 'Magento_Ui/js/form/element/abstract', 'config' => [ // customScope is used to group elements within a single form e.g. they can be validated separately @@ -309,10 +319,14 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi } /** + * Returns default attribute value. + * * @param string $attributeCode + * @throws NoSuchEntityException + * @throws LocalizedException * @return null|string */ - protected function getDefaultValue($attributeCode) + protected function getDefaultValue($attributeCode): ?string { if ($attributeCode === 'country_id') { return $this->directoryHelper->getDefaultCountry(); @@ -346,9 +360,13 @@ protected function getDefaultValue($attributeCode) } /** - * @return \Magento\Customer\Api\Data\CustomerInterface|null + * Returns logged customer. + * + * @throws NoSuchEntityException + * @throws LocalizedException + * @return CustomerInterface|null */ - protected function getCustomer() + protected function getCustomer(): ?CustomerInterface { if (!$this->customer) { if ($this->customerSession->isLoggedIn()) { diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php index 3f6f638db5b82..557f143352446 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php @@ -122,6 +122,7 @@ private function convertElementsToSelect($elements, $attributesToConvert) if (!in_array($code, $codes)) { continue; } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $options = call_user_func($attributesToConvert[$code]); if (!is_array($options)) { continue; @@ -287,8 +288,14 @@ private function getBillingAddressComponent($paymentCode, $elements) 'provider' => 'checkoutProvider', 'deps' => 'checkoutProvider', 'dataScopePrefix' => 'billingAddress' . $paymentCode, + 'billingAddressListProvider' => '${$.name}.billingAddressList', 'sortOrder' => 1, 'children' => [ + 'billingAddressList' => [ + 'component' => 'Magento_Checkout/js/view/billing-address/list', + 'displayArea' => 'billing-address-list', + 'template' => 'Magento_Checkout/billing-address/list' + ], 'form-fields' => [ 'component' => 'uiComponent', 'displayArea' => 'additional-fieldsets', diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php index ca6b045ddbb5d..e01d5835b4cf0 100644 --- a/app/code/Magento/Checkout/Block/Onepage.php +++ b/app/code/Magento/Checkout/Block/Onepage.php @@ -38,7 +38,7 @@ class Onepage extends \Magento\Framework\View\Element\Template protected $layoutProcessors; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var \Magento\Framework\Serialize\SerializerInterface */ private $serializer; @@ -48,8 +48,9 @@ class Onepage extends \Magento\Framework\View\Element\Template * @param \Magento\Checkout\Model\CompositeConfigProvider $configProvider * @param array $layoutProcessors * @param array $data - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer - * @throws \RuntimeException + * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param \Magento\Framework\Serialize\SerializerInterface $serializerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -57,7 +58,8 @@ public function __construct( \Magento\Checkout\Model\CompositeConfigProvider $configProvider, array $layoutProcessors = [], array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + \Magento\Framework\Serialize\SerializerInterface $serializerInterface = null ) { parent::__construct($context, $data); $this->formKey = $formKey; @@ -65,12 +67,12 @@ public function __construct( $this->jsLayout = isset($data['jsLayout']) && is_array($data['jsLayout']) ? $data['jsLayout'] : []; $this->configProvider = $configProvider; $this->layoutProcessors = $layoutProcessors; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializerInterface ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); } /** - * @return string + * @inheritdoc */ public function getJsLayout() { @@ -78,7 +80,7 @@ public function getJsLayout() $this->jsLayout = $processor->process($this->jsLayout); } - return json_encode($this->jsLayout, JSON_HEX_TAG); + return $this->serializer->serialize($this->jsLayout); } /** @@ -115,11 +117,13 @@ public function getBaseUrl() } /** + * Retrieve serialized checkout config. + * * @return bool|string * @since 100.2.0 */ public function getSerializedCheckoutConfig() { - return json_encode($this->getCheckoutConfig(), JSON_HEX_TAG); + return $this->serializer->serialize($this->getCheckoutConfig()); } } diff --git a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php index 6b3774f7e38f8..3b2f1604fae44 100644 --- a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php +++ b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php @@ -8,6 +8,8 @@ use Magento\Framework\View\Element\Template; /** + * Displays buttons on shopping cart page + * * @api */ class QuoteShortcutButtons extends \Magento\Catalog\Block\ShortcutButtons @@ -45,7 +47,8 @@ protected function _beforeToHtml() 'container' => $this, 'is_catalog_product' => $this->_isCatalogProduct, 'or_position' => $this->_orPosition, - 'checkout_session' => $this->_checkoutSession + 'checkout_session' => $this->_checkoutSession, + 'is_shopping_cart' => true ] ); return $this; diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php index c205f3c16072f..7eb9362031258 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php +++ b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php @@ -4,17 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Checkout\Controller\Cart; use Magento\Checkout\Model\Cart as CustomerCart; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Escaper; use Magento\Framework\App\ObjectManager; use Magento\Sales\Model\Order\Item; /** + * Add grouped items controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Addgroup extends \Magento\Checkout\Controller\Cart +class Addgroup extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface { /** * @var Escaper @@ -44,6 +48,8 @@ public function __construct( } /** + * Add items in group. + * * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() @@ -74,6 +80,8 @@ public function execute() } } $this->cart->save(); + } else { + $this->messageManager->addErrorMessage(__('Please select at least one product to add to cart')); } return $this->_goBack(); } diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php index 01e91d75c02d9..169be4cc62f01 100644 --- a/app/code/Magento/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Checkout/CustomerData/Cart.php @@ -10,6 +10,8 @@ /** * Cart source + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Cart extends \Magento\Framework\DataObject implements SectionSourceInterface { @@ -98,7 +100,8 @@ public function getSectionData() 'items' => $this->getRecentItems(), 'extra_actions' => $this->layout->createBlock(\Magento\Catalog\Block\ShortcutButtons::class)->toHtml(), 'isGuestCheckoutAllowed' => $this->isGuestCheckoutAllowed(), - 'website_id' => $this->getQuote()->getStore()->getWebsiteId() + 'website_id' => $this->getQuote()->getStore()->getWebsiteId(), + 'storeId' => $this->getQuote()->getStore()->getStoreId() ]; } diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index eff07af0e6a3e..cec99909dc999 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -16,6 +16,7 @@ * Shopping cart model * * @api + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead * @see \Magento\Quote\Api\Data\CartInterface @@ -365,20 +366,10 @@ protected function _getProductRequest($requestInfo) public function addProduct($productInfo, $requestInfo = null) { $product = $this->_getProduct($productInfo); - $request = $this->_getProductRequest($requestInfo); $productId = $product->getId(); if ($productId) { - $stockItem = $this->stockRegistry->getStockItem($productId, $product->getStore()->getWebsiteId()); - $minimumQty = $stockItem->getMinSaleQty(); - //If product quantity is not specified in request and there is set minimal qty for it - if ($minimumQty - && $minimumQty > 0 - && !$request->getQty() - ) { - $request->setQty($minimumQty); - } - + $request = $this->getQtyRequest($product, $requestInfo); try { $this->_eventManager->dispatch( 'checkout_cart_product_add_before', @@ -438,8 +429,9 @@ public function addProductsByIds($productIds) } $product = $this->_getProduct($productId); if ($product->getId() && $product->isVisibleInCatalog()) { + $request = $this->getQtyRequest($product); try { - $this->getQuote()->addProduct($product); + $this->getQuote()->addProduct($product, $request); } catch (\Exception $e) { $allAdded = false; } @@ -762,4 +754,27 @@ private function getRequestInfoFilter() } return $this->requestInfoFilter; } + + /** + * Get request quantity + * + * @param Product $product + * @param \Magento\Framework\DataObject|int|array $request + * @return int|DataObject + */ + private function getQtyRequest($product, $request = 0) + { + $request = $this->_getProductRequest($request); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $minimumQty = $stockItem->getMinSaleQty(); + //If product quantity is not specified in request and there is set minimal qty for it + if ($minimumQty + && $minimumQty > 0 + && !$request->getQty() + ) { + $request->setQty($minimumQty); + } + + return $request; + } } diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php index f30bd73deeae2..470d4a3aca561 100644 --- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php +++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php @@ -10,6 +10,7 @@ use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\Address\CustomerAddressDataProvider; use Magento\Customer\Model\Context as CustomerContext; use Magento\Customer\Model\Session as CustomerSession; use Magento\Customer\Model\Url as CustomerUrlManager; @@ -34,6 +35,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class DefaultConfigProvider implements ConfigProviderInterface { @@ -177,6 +179,11 @@ class DefaultConfigProvider implements ConfigProviderInterface */ private $addressMetadata; + /** + * @var CustomerAddressDataProvider + */ + private $customerAddressData; + /** * @param CheckoutHelper $checkoutHelper * @param Session $checkoutSession @@ -206,6 +213,7 @@ class DefaultConfigProvider implements ConfigProviderInterface * @param UrlInterface $urlBuilder * @param AddressMetadataInterface $addressMetadata * @param AttributeOptionManagementInterface $attributeOptionManager + * @param CustomerAddressDataProvider|null $customerAddressData * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -237,7 +245,8 @@ public function __construct( \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement, UrlInterface $urlBuilder, AddressMetadataInterface $addressMetadata = null, - AttributeOptionManagementInterface $attributeOptionManager = null + AttributeOptionManagementInterface $attributeOptionManager = null, + CustomerAddressDataProvider $customerAddressData = null ) { $this->checkoutHelper = $checkoutHelper; $this->checkoutSession = $checkoutSession; @@ -268,6 +277,8 @@ public function __construct( $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); $this->attributeOptionManager = $attributeOptionManager ?? ObjectManager::getInstance()->get(AttributeOptionManagementInterface::class); + $this->customerAddressData = $customerAddressData ?: + ObjectManager::getInstance()->get(CustomerAddressDataProvider::class); } /** @@ -359,57 +370,18 @@ private function isAutocompleteEnabled() * * @return array */ - private function getCustomerData() + private function getCustomerData(): array { $customerData = []; if ($this->isCustomerLoggedIn()) { + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ $customer = $this->customerRepository->getById($this->customerSession->getCustomerId()); $customerData = $customer->__toArray(); - foreach ($customer->getAddresses() as $key => $address) { - $customerData['addresses'][$key]['inline'] = $this->getCustomerAddressInline($address); - if ($address->getCustomAttributes()) { - $customerData['addresses'][$key]['custom_attributes'] = $this->filterNotVisibleAttributes( - $customerData['addresses'][$key]['custom_attributes'] - ); - } - } + $customerData['addresses'] = $this->customerAddressData->getAddressDataByCustomer($customer); } return $customerData; } - /** - * Filter not visible on storefront custom attributes. - * - * @param array $attributes - * @return array - */ - private function filterNotVisibleAttributes(array $attributes) - { - $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); - foreach ($attributesMetadata as $attributeMetadata) { - if (!$attributeMetadata->isVisible()) { - unset($attributes[$attributeMetadata->getAttributeCode()]); - } - } - - return $this->setLabelsToAttributes($attributes); - } - - /** - * Set additional customer address data - * - * @param \Magento\Customer\Api\Data\AddressInterface $address - * @return string - */ - private function getCustomerAddressInline($address) - { - $builtOutputAddressData = $this->addressMapper->toFlatArray($address); - return $this->addressConfig - ->getFormatByCode(\Magento\Customer\Model\Address\Config::DEFAULT_ADDRESS_FORMAT) - ->getRenderer() - ->renderArray($builtOutputAddressData); - } - /** * Retrieve quote data * @@ -726,61 +698,6 @@ private function getPaymentMethods() return $paymentMethods; } - /** - * Set Labels to custom Attributes - * - * @param array $customAttributes - * @return array $customAttributes - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\StateException - */ - private function setLabelsToAttributes(array $customAttributes) : array - { - if (!empty($customAttributes)) { - foreach ($customAttributes as $customAttributeCode => $customAttribute) { - $attributeOptionLabels = $this->getAttributeLabels($customAttribute, $customAttributeCode); - if (!empty($attributeOptionLabels)) { - $customAttributes[$customAttributeCode]['label'] = implode(', ', $attributeOptionLabels); - } - } - } - - return $customAttributes; - } - - /** - * Get Labels by CustomAttribute and CustomAttributeCode - * - * @param array $customAttribute - * @param string|integer $customAttributeCode - * @return array $attributeOptionLabels - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\StateException - */ - private function getAttributeLabels(array $customAttribute, string $customAttributeCode) : array - { - $attributeOptionLabels = []; - - if (!empty($customAttribute['value'])) { - $customAttributeValues = explode(',', $customAttribute['value']); - $attributeOptions = $this->attributeOptionManager->getItems( - \Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, - $customAttributeCode - ); - - if (!empty($attributeOptions)) { - foreach ($attributeOptions as $attributeOption) { - $attributeOptionValue = $attributeOption->getValue(); - if (in_array($attributeOptionValue, $customAttributeValues)) { - $attributeOptionLabels[] = $attributeOption->getLabel() ?? $attributeOptionValue; - } - } - } - } - - return $attributeOptionLabels; - } - /** * Get notification messages for the quote items * diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 333226b7d216f..da29482f0123f 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -14,6 +14,8 @@ use Magento\Quote\Model\Quote; /** + * Guest payment information management model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPaymentInformationManagementInterface @@ -66,7 +68,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository - * @param ResourceConnection|null + * @param ResourceConnection $connectionPool * @codeCoverageIgnore */ public function __construct( @@ -88,7 +90,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformationAndPlaceOrder( $cartId, @@ -129,7 +131,7 @@ public function savePaymentInformationAndPlaceOrder( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformation( $cartId, @@ -156,7 +158,7 @@ public function savePaymentInformation( } /** - * {@inheritDoc} + * @inheritdoc */ public function getPaymentInformation($cartId) { @@ -190,9 +192,8 @@ private function limitShippingCarrier(Quote $quote) : void { $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { - $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); - $shippingCarrier = array_shift($shippingDataArray); - $shippingAddress->setLimitCarrier($shippingCarrier); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); } } } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index d2bd680aa38f3..e0de45a3f0dea 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -118,7 +118,9 @@ public function savePaymentInformation( $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); - $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); + $shippingAddress->setLimitCarrier( + $shippingRate ? $shippingRate->getCarrier() : $shippingAddress->getShippingMethod() + ); } } $this->paymentMethodManagement->set($cartId, $paymentMethod); diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml new file mode 100644 index 0000000000000..8c5c6f41fffa7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.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="AssertMiniShoppingCartSubTotalActionGroup"> + <arguments> + <argument name="dataQuote" type="entity" /> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad" time="120"/> + <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabMiniCartTotal" /> + <assertContains stepKey="assertMiniCartTotal"> + <actualResult type="variable">$grabMiniCartTotal</actualResult> + <expectedResult type="string">{{dataQuote.subtotal}}</expectedResult> + </assertContains> + <assertContains stepKey="assertMiniCartCurrency"> + <actualResult type="variable">$grabMiniCartTotal</actualResult> + <expectedResult type="string">{{dataQuote.currency}}</expectedResult> + </assertContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..c4fc753e73713 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.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="AssertStorefrontEmailNoteMessageOnCheckoutActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="You can create an account after checkout." /> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailNoteMessage}}" stepKey="waitForFormValidation"/> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailNoteMessage}}" userInput="{{message}}" stepKey="seeTheNoteMessageIsDisplayed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..f9c6771262ccc --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.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="AssertStorefrontEmailTooltipContentOnCheckoutActionGroup"> + <arguments> + <argument name="content" type="string" defaultValue="We'll send your order confirmation here." /> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="waitForTooltipButtonVisible" /> + <click selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickEmailTooltipButton" /> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipContent}}" userInput="{{content}}" stepKey="seeEmailTooltipContent" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..14b96ed46ce6b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.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="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Please enter a valid email address (Ex: johndoe@domain.com)." /> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" stepKey="waitForFormValidation"/> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" userInput="{{message}}" stepKey="seeTheErrorMessageIsDisplayed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index 464ccc1913335..2e4b742ece8ec 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -17,6 +17,7 @@ <!-- Go to checkout from minicart --> <actionGroup name="GoToCheckoutFromMinicartActionGroup"> <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> + <wait time="5" stepKey="waitMinicartRendering"/> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> </actionGroup> @@ -109,7 +110,7 @@ <argument name="paymentMethod" type="string"/> </arguments> <remove keyForRemoval="checkMessage"/> - <dontsee selector="{{CheckoutPaymentSection.paymentMethodByName(paymentMethod)}}" parametrized="true" stepKey="paymentMethodDoesNotAvailable"/> + <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName(paymentMethod)}}" stepKey="paymentMethodDoesNotAvailable"/> </actionGroup> <!-- Logged in user checkout filling shipping section --> <actionGroup name="LoggedInUserCheckoutFillingShippingSectionActionGroup"> @@ -187,16 +188,16 @@ <!-- Check order summary in checkout --> <actionGroup name="CheckOrderSummaryInCheckoutActionGroup"> <arguments> - <argument name="subtotal"/> - <argument name="shippingTotal"/> - <argument name="shippingMethod"/> - <argument name="total"/> + <argument name="subtotal" type="string"/> + <argument name="shippingTotal" type="string"/> + <argument name="shippingMethod" type="string"/> + <argument name="total" type="string"/> </arguments> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <see userInput="${{subtotal}}" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> - <see userInput="${{shippingTotal}}" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> + <see userInput="{{subtotal}}" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> + <see userInput="{{shippingTotal}}" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> <see userInput="{{shippingMethod}}" selector="{{CheckoutPaymentSection.orderSummaryShippingMethod}}" stepKey="assertShippingMethod"/> - <see userInput="${{total}}" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> + <see userInput="{{total}}" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> </actionGroup> <actionGroup name="CheckTotalsSortOrderInSummarySection"> @@ -240,6 +241,21 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> </actionGroup> + <!-- Check selected shipping address information on shipping information step --> + <actionGroup name="CheckSelectedShippingAddressInCheckoutActionGroup"> + <arguments> + <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}}" /> + </actionGroup> + <!-- Check billing address in checkout --> <actionGroup name="CheckBillingAddressInCheckoutActionGroup"> <arguments> @@ -256,13 +272,29 @@ <see userInput="{{customerAddressVar.telephone}}" selector="{{CheckoutPaymentSection.billingAddress}}" stepKey="assertBillingAddressTelephone"/> </actionGroup> + <!-- Check billing address in checkout with billing address on payment page --> + <actionGroup name="CheckBillingAddressInCheckoutWithBillingAddressOnPaymentPageActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <see userInput="{{customerVar.firstName}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsFirstName"/> + <see userInput="{{customerVar.lastName}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsLastName"/> + <see userInput="{{customerAddressVar.street[0]}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsStreet"/> + <see userInput="{{customerAddressVar.city}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsCity"/> + <see userInput="{{customerAddressVar.state}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsState"/> + <see userInput="{{customerAddressVar.postcode}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsPostcode"/> + <see userInput="{{customerAddressVar.telephone}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsTelephone"/> + </actionGroup> + <!-- Checkout place order --> <actionGroup name="CheckoutPlaceOrderActionGroup"> <arguments> <argument name="orderNumberMessage"/> <argument name="emailYouMessage"/> </arguments> - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{orderNumberMessage}}" stepKey="seeOrderNumber"/> <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{emailYouMessage}}" stepKey="seeEmailYou"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..0e6994e8feaa4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details.z + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ClearShippingAddressActionGroup"> + <clearField selector="{{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> + <clearField selector="{{CheckoutShippingSection.company}}" stepKey="clearFieldCompany"/> + <clearField selector="{{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress"/> + <clearField selector="{{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> + <clearField selector="{{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> + <clearField selector="{{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml new file mode 100644 index 0000000000000..7035855cc0ed3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.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="FillNewShippingAddressModalActionGroup" extends="FillShippingAddressOneStreetActionGroup"> + <arguments> + <argument name="address"/> + </arguments> + <selectOption stepKey="selectRegion" selector="{{CheckoutShippingSection.region}}" + userInput="{{address.state}}" after="fillCityName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml new file mode 100644 index 0000000000000..cbc6c5320b01c --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml @@ -0,0 +1,24 @@ +<?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="FillShippingAddressOneStreetActionGroup"> + <arguments> + <argument name="address" type="entity"/> + </arguments> + <fillField stepKey="fillFirstName" selector="{{CheckoutShippingSection.firstName}}" userInput="{{address.firstname}}"/> + <fillField stepKey="fillLastName" selector="{{CheckoutShippingSection.lastName}}" userInput="{{address.lastname}}"/> + <fillField stepKey="fillCompany" selector="{{CheckoutShippingSection.company}}" userInput="{{address.company}}"/> + <fillField stepKey="fillPhoneNumber" selector="{{CheckoutShippingSection.telephone}}" userInput="{{address.telephone}}"/> + <fillField stepKey="fillStreetAddress" selector="{{CheckoutShippingSection.street}}" userInput="{{address.street[0]}}"/> + <fillField stepKey="fillCityName" selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}"/> + <selectOption stepKey="selectCounty" selector="{{CheckoutShippingSection.country}}" userInput="{{address.country_id}}"/> + <fillField stepKey="fillZip" selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml index 6e5f127eefc18..15c157a982643 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Assert That Shipping And Billing Address are the same--> <actionGroup name="AssertThatShippingAndBillingAddressTheSame"> <!--Get shipping and billing addresses--> @@ -18,5 +18,4 @@ <see userInput="Billing Address" stepKey="seeBillingAddress"/> <assertEquals stepKey="assert" actual="$billingAddr" expected="$shippingAddr"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml new file mode 100644 index 0000000000000..7bfc87cd8d6f9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.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"> + <!-- Add Product to Cart from the category page and check message --> + <actionGroup name="StorefrontAddSimpleProductToCartActionGroup"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml new file mode 100644 index 0000000000000..0d6f34098c048 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.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"> + <!-- Assert Estimate Shipping and Tax Data in Cart --> + <actionGroup name="StorefrontAssertCartEstimateShippingAndTaxActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml new file mode 100644 index 0000000000000..4061f97821cd0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.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"> + <!-- Assert Shipping Method Is Checked on Cart --> + <actionGroup name="StorefrontAssertCartShippingMethodSelectedActionGroup"> + <arguments> + <argument name="carrierCode" type="string"/> + <argument name="methodCode" type="string"/> + </arguments> + <seeCheckboxIsChecked selector="{{CheckoutCartSummarySection.shippingMethodElementId(carrierCode, methodCode)}}" stepKey="assertShippingMethodIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml new file mode 100644 index 0000000000000..82d7e12105b8c --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.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"> + <!-- Assert Estimate Shipping and Tax Data on Checkout --> + <actionGroup name="StorefrontAssertCheckoutEstimateShippingInformationActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryField"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceField"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml new file mode 100644 index 0000000000000..33f2852f1f0ad --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.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"> + <!-- Assert Shipping Method by Name Is Checked on Checkout --> + <actionGroup name="StorefrontAssertCheckoutShippingMethodSelectedActionGroup"> + <arguments> + <argument name="shippingMethod"/> + </arguments> + <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="assertShippingMethodByNameIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml new file mode 100644 index 0000000000000..02c362bf34058 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.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"> + <!-- Assert guest shipping info on checkout --> + <actionGroup name="StorefrontAssertGuestShippingInfoActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="assertEmailAddress"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="assertFirstName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="assertLastName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="assertCompany"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="assertAddressFirstLine"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="assertAddressSecondLine"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="assertCity"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountry"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvince"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCode"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="assertPhoneNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml new file mode 100644 index 0000000000000..3d8530ae83704 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.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"> + <!-- Assert shipping method is present in cart --> + <actionGroup name="StorefrontAssertShippingMethodPresentInCartActionGroup"> + <arguments> + <argument name="shippingMethod" type="string"/> + </arguments> + <see selector="{{CheckoutCartSummarySection.shippingMethodLabel}}" userInput="{{shippingMethod}}" stepKey="assertShippingMethodIsPresentInCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml new file mode 100644 index 0000000000000..4176859f99f70 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.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"> + <!-- Fill Estimate Shipping and Tax fields --> + <actionGroup name="StorefrontCartEstimateShippingAndTaxActionGroup"> + <arguments> + <argument name="estimateAddress" defaultValue="EstimateAddressCalifornia"/> + </arguments> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="clickOnEstimateShippingAndTax"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.country}}" stepKey="waitForCountrySelectorIsVisible"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="{{estimateAddress.country}}" stepKey="selectCountry"/> + <waitForLoadingMaskToDisappear stepKey="waitForCountryLoadingMaskDisappear"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{estimateAddress.state}}" stepKey="selectStateProvince"/> + <waitForLoadingMaskToDisappear stepKey="waitForStateLoadingMaskDisappear"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{estimateAddress.zipCode}}" stepKey="fillZipPostalCodeField"/> + <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..fcac780a36776 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.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="StorefrontFillEmailFieldOnCheckoutActionGroup"> + <arguments> + <argument name="email" type="string" /> + </arguments> + <fillField selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.email}}" userInput="{{email}}" stepKey="fillCustomerEmailField"/> + <doubleClick selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickToMoveFocusFromEmailInput" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml new file mode 100644 index 0000000000000..e7669d62c79a0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.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"> + <!-- Fill data in checkout shipping section --> + <actionGroup name="StorefrontFillGuestShippingInfoActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> + </arguments> + <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="fillEmailAddressField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="fillFirstNameField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="fillLastNameField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="fillCompanyField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="fillStreetAddressFirstLineField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="fillStreetAddressSecondLineField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="fillCityField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="fillPhoneNumberField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml index 7a5c5e1d15872..e4a388d2c3a58 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml @@ -35,4 +35,11 @@ <click selector="{{StoreFrontRemoveItemModalSection.ok}}" stepKey="confirmDelete"/> <waitForPageLoad stepKey="waitForDeleteToFinish"/> </actionGroup> + + <!--Check that the minicart is empty--> + <actionGroup name="assertMiniCartEmpty"> + <dontSeeElement selector="{{StorefrontMinicartSection.productCount}}" stepKey="dontSeeMinicartProductCount"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="expandMinicart"/> + <see selector="{{StorefrontMinicartSection.minicartContent}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml new file mode 100644 index 0000000000000..b18d476c02c65 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml @@ -0,0 +1,14 @@ +<?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="StorefrontOpenCheckoutPageActionGroup"> + <amOnPage url="{{CheckoutPage.url}}" stepKey="openCheckoutPage" /> + <waitForPageLoad stepKey="waitForCheckoutPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml index 72c5648991ef5..24ed05583b6fb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -78,20 +78,20 @@ <!-- Check the Cart --> <actionGroup name="StorefrontCheckCartActionGroup"> <arguments> - <argument name="subtotal"/> - <argument name="shipping"/> - <argument name="shippingMethod"/> - <argument name="total"/> + <argument name="subtotal" type="string"/> + <argument name="shipping" type="string"/> + <argument name="shippingMethod" type="string"/> + <argument name="total" type="string"/> </arguments> <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertUrl"/> <waitForPageLoad stepKey="waitForCartPage"/> <conditionalClick selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.shippingMethodForm}}" visible="false" stepKey="openEstimateShippingSection"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="waitForShippingSection"/> <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectShippingMethod"/> - <see userInput="${{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> + <see userInput="{{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> <see userInput="({{shippingMethod}})" selector="{{CheckoutCartSummarySection.shippingMethod}}" stepKey="assertShippingMethod"/> - <waitForText userInput="${{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping"/> - <see userInput="${{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> + <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping"/> + <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> </actionGroup> <!-- Open the Cart from Minicart--> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml index 354ad6d2b44ba..d3d96cb9c743c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Fill shipment form for free shipping--> <actionGroup name="ShipmentFormFreeShippingActionGroup"> <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="setCustomerEmail"/> @@ -26,4 +26,4 @@ <waitForPageLoad time="5" stepKey="waitForReviewAndPaymentsPageIsLoaded"/> <seeInCurrentUrl url="payment" stepKey="reviewAndPaymentIsShown"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..ee8b761a452d4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.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="StorefrontUpdateProductQtyMiniShoppingCartActionGroup"> + <arguments> + <argument name="product" type="entity" /> + <argument name="quote" type="entity" /> + </arguments> + + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="goToMiniShoppingCart"/> + + <!-- Clearing QTY field --> + <doubleClick selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" stepKey="doubleClickOnQtyInput" /> + <pressKey selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" parameterArray="[\WebDriverKeys::DELETE]" stepKey="clearQty"/> + <!-- Clearing QTY field --> + + <fillField selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" userInput="{{quote.qty}}" stepKey="changeQty"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdateBySku(product.sku)}}" stepKey="clickUpdateButton"/> + <waitForPageLoad stepKey="waitForProductQtyUpdate" /> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml index dc82932ec5ca7..7fc349bf9f05c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="Countries" type="countryArray"> <array key="country"> <item>Bahamas</item> @@ -35,4 +35,4 @@ <item>United Kingdom</item> </array> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml new file mode 100644 index 0000000000000..36dea5a521a04 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.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="EstimateAddressCalifornia"> + <data key="country">United States</data> + <data key="state">California</data> + <data key="zipCode">90240</data> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml index 530157851191f..e7a5992ad8943 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml @@ -15,4 +15,26 @@ <data key="total">495.00</data> <data key="shippingMethod">Flat Rate - Fixed</data> </entity> + <entity name="simpleOrderQty2" type="Quote"> + <data key="price">560.00</data> + <data key="qty">2</data> + <data key="subtotal">1,120.00</data> + <data key="shipping">10.00</data> + <data key="total">1,130.00</data> + <data key="shippingMethod">Flat Rate - Fixed</data> + <data key="currency">$</data> + </entity> + <entity name="quoteQty3Price123" type="Quote"> + <data key="price">123.00</data> + <data key="qty">3</data> + <data key="subtotal">369.00</data> + <data key="currency">$</data> + </entity> + <entity name="quoteQty11Subtotal1320" type="Quote"> + <data key="price">100.00</data> + <data key="customOptionsPrice">20</data> + <data key="qty">11</data> + <data key="subtotal">1,320.00</data> + <data key="currency">$</data> + </entity> </entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml index b0acc64c77727..bf17800f29ad1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml @@ -11,5 +11,6 @@ <page name="CheckoutCartPage" url="/checkout/cart" module="Magento_Checkout" area="storefront"> <section name="CheckoutCartProductSection"/> <section name="CheckoutCartSummarySection"/> + <section name="CheckoutCartCrossSellSection"/> </page> </pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml new file mode 100644 index 0000000000000..aa23f3364771f --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutCartCrossSellSection"> + <element name="title" type="text" selector=".block.crosssell .block-title"/> + <element name="products" type="block" selector=".block.crosssell .block-content"/> + <element name="productRowByName" type="block" selector="//li[@class='item product product-item'and .//a[@title='{{name}}']]" parameterized="true"/> + <element name="addToCart" type="block" selector="//button[@title='Add to Cart']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index ab82d9fdd93b5..dcfb12fd4e965 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -26,10 +26,12 @@ parameterized="true"/> <element name="RemoveItem" type="button" selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/> + <element name="productName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']"/> <element name="nthItemOption" type="block" selector=".item:nth-of-type({{numElement}}) .item-options" parameterized="true"/> <element name="nthEditButton" type="block" selector=".item:nth-of-type({{numElement}}) .action-edit" parameterized="true"/> <element name="nthBundleOptionName" type="text" selector=".product-item-details .item-options:nth-of-type({{numOption}}) dt" parameterized="true"/> <element name="productSubtotalByName" type="input" selector="//main//table[@id='shopping-cart-table']//tbody//tr[..//strong[contains(@class, 'product-item-name')]//a/text()='{{var1}}'][1]//td[contains(@class, 'subtotal')]//span[@class='price']" parameterized="true"/> <element name="updateShoppingCartButton" type="button" selector="#form-validate button[type='submit'].update" timeout="30"/> + <element name="qty" type="input" selector="//input[@data-cart-item-id='{{var}}'][@title='Qty']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index d84df3401bab0..3100fae3b119b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -20,9 +20,12 @@ <element name="shippingHeading" type="button" selector="#block-shipping-heading"/> <element name="postcode" type="input" selector="input[name='postcode']" timeout="10"/> <element name="stateProvince" type="select" selector="select[name='region_id']" timeout="10"/> + <element name="stateProvinceInput" type="input" selector="input[name='region']"/> <element name="country" type="select" selector="select[name='country_id']" timeout="10"/> <element name="countryParameterized" type="select" selector="select[name='country_id'] > option:nth-child({{var}})" timeout="10" parameterized="true"/> <element name="estimateShippingAndTax" type="text" selector="#block-shipping-heading" timeout="5"/> - <element name="flatRateShippingMethod" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> + <element name="flatRateShippingMethod" type="input" selector="#s_method_flatrate_flatrate" timeout="30"/> + <element name="shippingMethodLabel" type="text" selector="#co-shipping-method-form dl dt span"/> + <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 0206c18b819c2..cbe71e9cffa60 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -54,5 +54,6 @@ <element name="addressBook" type="button" selector="//a[text()='Address Book']"/> <element name="noQuotes" type="text" selector=".no-quotes-block"/> <element name="paymentMethodByName" type="text" selector="//*[@id='checkout-payment-method-load']//*[contains(@class, 'payment-group')]//label[normalize-space(.)='{{var1}}']" parameterized="true"/> + <element name="billingAddressSelectShared" type="select" selector=".checkout-billing-address select[name='billing_address_id']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml new file mode 100644 index 0000000000000..42decd8d43220 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.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="CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection"> + <element name="billingAddressDetails" type="text" selector="div.billing-address-details"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index ad2a43eb90c8c..b19e365f2e32c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -9,15 +9,23 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutShippingGuestInfoSection"> - <element name="email" type="input" selector="#customer-email"/> + <element name="email" type="input" selector="#checkout-customer-email"/> <element name="firstName" type="input" selector="input[name=firstname]"/> <element name="lastName" type="input" selector="input[name=lastname]"/> <element name="street" type="input" selector="input[name='street[0]']"/> + <element name="street2" type="input" selector="input[name='street[1]']"/> <element name="city" type="input" selector="input[name=city]"/> <element name="region" type="select" selector="select[name=region_id]"/> + <element name="regionInput" type="input" selector="input[name=region]"/> <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="country" type="select" selector="select[name=country_id]"/> + <element name="company" type="input" selector="input[name=company]"/> <element name="telephone" type="input" selector="input[name=telephone]"/> <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector=".row:nth-of-type(1) .col-method .radio"/> + + <!--Order Summary--> + <element name="itemInCart" type="button" selector="//div[@class='title']"/> + <element name="productName" type="text" selector="//strong[@class='product-item-name']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml index ab4b59fd67d03..77d903eab3934 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml @@ -17,5 +17,6 @@ <element name="shippingMethodRowByName" type="text" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/.." parameterized="true"/> <element name="shipHereButton" type="button" selector="//div/following-sibling::div/button[contains(@class, 'action-select-shipping-item')]"/> <element name="shippingMethodLoader" type="button" selector="//div[contains(@class, 'checkout-shipping-method')]/following-sibling::div[contains(@class, 'loading-mask')]"/> + <element name="freeShippingShippingMethod" type="input" selector="#s_method_freeshipping_freeshipping" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index a182a3357a9ce..9676f16f3a5c6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -15,7 +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="email" type="input" selector="#customer-email"/> + <element name="email" type="input" selector="#checkout-customer-email"/> <element name="firstName" type="input" selector="input[name=firstname]"/> <element name="lastName" type="input" selector="input[name=lastname]"/> <element name="company" type="input" selector="input[name=company]"/> @@ -27,6 +27,7 @@ <element name="country" type="select" selector="select[name=country_id]"/> <element name="telephone" type="input" selector="input[name=telephone]"/> <element name="saveAddress" type="button" selector=".action-save-address"/> + <element name="cancelChangeAddress" type="button" selector=".action-hide-popup"/> <element name="updateAddress" type="button" selector=".action-update"/> <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml index 34819f641cbc9..bc65f8a2c0816 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml @@ -16,6 +16,7 @@ <element name="orderLink" type="text" selector="a[href*=order_id].order-number" timeout="30"/> <element name="orderNumberText" type="text" selector=".checkout-success > p:nth-child(1)"/> <element name="continueShoppingButton" type="button" selector=".action.primary.continue" timeout="30"/> + <element name="createAnAccount" type="button" selector="input[value='Create an Account']" timeout="30"/> <element name="printLink" type="button" selector=".print" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml index d0ef8d347efb5..0d692e4ab143e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml @@ -12,5 +12,6 @@ <element name="registerMessage" type="text" selector="#registration p:nth-child(1)"/> <element name="customerEmail" type="text" selector="#registration p:nth-child(2)"/> <element name="createAccountButton" type="button" selector="#registration form input[type='submit']" timeout="30"/> + <element name="orderNumber" type="text" selector="//p[text()='Your order # is: ']//span"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml index 89b3a25b45e3c..2039128ac2de3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml @@ -7,8 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ShipmentFormSection"> <element name="shippingAddress" type="textarea" selector="//*[@class='box box-billing-address']//address"/> <element name="billingAddress" type="textarea" selector="//*[@class='box box-shipping-address']//address"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml new file mode 100644 index 0000000000000..9772fa1993acb --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml @@ -0,0 +1,22 @@ +<?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="StorefrontCheckoutCheckoutCustomerLoginSection"> + <element name="email" type="input" selector="form[data-role='email-with-possible-login'] input[name='username']" /> + <element name="emailNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'note')]" /> + <element name="emailErrorMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[@id='checkout-customer-email-error']" /> + <element name="emailTooltipButton" type="button" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'action-help')]" /> + <element name="emailTooltipContent" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'field-tooltip-content')]" /> + <element name="password" type="input" selector="form[data-role='email-with-possible-login'] input[name='password']" /> + <element name="passwordNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='password']]//*[contains(@class, 'note')]" /> + <element name="submit" type="button" selector="form[data-role='email-with-possible-login'] button[type='submit']" /> + <element name="forgotPassword" type="button" selector="form[data-role='email-with-possible-login'] a.remind" /> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml index 55c4385706ba9..9d9a96d2ea5e6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml @@ -12,5 +12,6 @@ <element name="billingAddress" type="text" selector=".checkout-billing-address"/> <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[@class='payment-method']//label//span[contains(., '{{methodName}}')]/../..//input" parameterized="true"/> <element name="billingAddressSameAsShipping" type="checkbox" selector=".payment-method._active [name='billing-address-same-as-shipping']"/> + <element name="billingAddressSameAsShippingShared" type="checkbox" selector="#billing-address-same-as-shipping-shared"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index a894f2fbb1af9..38c88bf4f80bb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -13,6 +13,7 @@ <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> + <element name="productName" type="text" selector=".product-item-name"/> <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> <element name="showCart" type="button" selector="a.showcart"/> @@ -24,6 +25,8 @@ <element name="deleteMiniCartItem" type="button" selector=".action.delete" timeout="30"/> <element name="deleteMiniCartItemByName" type="button" selector="//ol[@id='mini-cart']//div[contains(., '{{var}}')]//a[contains(@class, 'delete')]" parameterized="true"/> <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> + <element name="itemQuantityBySku" type="input" selector="#minicart-content-wrapper input[data-cart-item-id='{{productSku}}']" parameterized="true"/> + <element name="itemQuantityUpdateBySku" type="button" selector="//div[@id='minicart-content-wrapper']//input[@data-cart-item-id='{{productSku}}']/../button[contains(@class, 'update-cart-item')]" parameterized="true"/> <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> <element name="itemDiscount" type="text" selector="//tr[@class='totals']//td[@class='amount']/span"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml index 269ca94b3f772..f3807388399b8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml @@ -1,86 +1,86 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="CheckoutSpecificDestinationsTest"> - <annotations> - <title value="Check that top destinations can be removed after a selection was previously saved"/> - <stories value="MAGETWO-91511: Top destinations cannot be removed after a selection was previously saved"/> - <description value="Check that top destinations can be removed after a selection was previously saved"/> - <features value="Checkout"/> - <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-94195"/> - <group value="Checkout"/> - </annotations> - - <before> - <createData entity="_defaultCategory" stepKey="defaultCategory"/> - <createData entity="_defaultProduct" stepKey="simpleProduct"> - <requiredEntity createDataKey="defaultCategory"/> - </createData> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - </before> - - <!--Go to configuration general page--> - <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage"/> - - <!--Open country options section--> - <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation"/> - - <!--Select top destinations country--> - <actionGroup ref="SelectTopDestinationsCountry" stepKey="selectTopDestinationsCountry"> - <argument name="countries" value="Countries"/> - </actionGroup> - - <!--Go to product page--> - <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="amOnStorefrontProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - - <!--Add product to cart--> - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCart"> - <argument name="productName" value="$$simpleProduct.name$$"/> - </actionGroup> - - <!--Go to shopping cart--> - <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> - - <!--Verify country options in checkout top destination section--> - <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry"> - <argument name="country" value="Bahamas"/> - <argument name="placeNumber" value="2"/> - </actionGroup> - - <!--Go to configuration general page--> - <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage2"/> - - <!--Open country options section--> - <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> - - <!--Deselect top destinations country--> - <actionGroup ref="UnSelectTopDestinationsCountry" stepKey="unSelectTopDestinationsCountry"> - <argument name="countries" value="Countries"/> - </actionGroup> - - <!--Go to shopping cart--> - <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart2"/> - - <!--Verify country options is shown by default--> - <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry2"> - <argument name="country" value="Afghanistan"/> - <argument name="placeNumber" value="2"/> - </actionGroup> - - <after> - <actionGroup ref="logout" stepKey="logout"/> - - <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> - </after> - </test> -</tests> \ No newline at end of file +<?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="CheckoutSpecificDestinationsTest"> + <annotations> + <title value="Check that top destinations can be removed after a selection was previously saved"/> + <stories value="MAGETWO-91511: Top destinations cannot be removed after a selection was previously saved"/> + <description value="Check that top destinations can be removed after a selection was previously saved"/> + <features value="Checkout"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-94195"/> + <group value="Checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Go to configuration general page--> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage"/> + + <!--Open country options section--> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation"/> + + <!--Select top destinations country--> + <actionGroup ref="SelectTopDestinationsCountry" stepKey="selectTopDestinationsCountry"> + <argument name="countries" value="Countries"/> + </actionGroup> + + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="amOnStorefrontProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!--Add product to cart--> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCart"> + <argument name="productName" value="$$simpleProduct.name$$"/> + </actionGroup> + + <!--Go to shopping cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + + <!--Verify country options in checkout top destination section--> + <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry"> + <argument name="country" value="Bahamas"/> + <argument name="placeNumber" value="2"/> + </actionGroup> + + <!--Go to configuration general page--> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage2"/> + + <!--Open country options section--> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> + + <!--Deselect top destinations country--> + <actionGroup ref="UnSelectTopDestinationsCountry" stepKey="unSelectTopDestinationsCountry"> + <argument name="countries" value="Countries"/> + </actionGroup> + + <!--Go to shopping cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart2"/> + + <!--Verify country options is shown by default--> + <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry2"> + <argument name="country" value="Afghanistan"/> + <argument name="placeNumber" value="2"/> + </actionGroup> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml new file mode 100644 index 0000000000000..166f5022d5aeb --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DefaultBillingAddressShouldBeCheckedOnPaymentPageTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="The default billing address should be used on checkout"/> + <description value="Default billing address should be preselected on payments page on checkout if it exist"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-98892"/> + <useCaseId value="MAGETWO-70996"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </after> + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInCheckoutWithOneAddressFieldWithoutStateField" stepKey="changeAddress"> + <argument name="Address" value="UK_Not_Default_Address"/> + <argument name="classPrefix" value="._show"/> + </actionGroup> + <!--Click "Save Addresses" --> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <dontSeeElement selector="{{StorefrontCheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + <!--Select Shipping Rate "Flat Rate" and click "Next" button--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!--Verify that "My billing and shipping address are the same" is unchecked and billing address is preselected--> + <dontSeeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="shippingAndBillingAddressIsSameUnchecked"/> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="assertBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml new file mode 100644 index 0000000000000..20015f76e08e3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -0,0 +1,80 @@ +<?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="EditShippingAddressOnePageCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Edit Shipping Address"/> + <title value="Edit Shipping Address on Checkout Page."/> + <description value="Edit Shipping Address on Checkout Page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14680"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="simpleProductDefault" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Go to Frontend as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Add product to cart --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <actionGroup ref="StorefrontAddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Go to checkout page --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="customerGoToCheckoutFromMinicart" /> + + <!-- *New Address* button on 1st checkout step --> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addNewAddress"/> + + <!--Fill in required fields and click *Save address* button--> + <actionGroup ref="FillShippingAddressOneStreetActionGroup" stepKey="changeAddress"> + <argument name="address" value="UK_Not_Default_Address"/> + </actionGroup> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveNewAddress"/> + + <!--Select Shipping Rate--> + <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToShippingRate"/> + <click selector="{{CheckoutShippingMethodsSection.shippingMethodFlatRate}}" stepKey="selectShippingMethod"/> + + <!-- Click *Edit* button for the new address --> + <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> + + <!--Remove values from required fields and click *Cancel* button --> + <actionGroup ref="ClearShippingAddressActionGroup" stepKey="clearRequiredFields"/> + <click selector="{{CheckoutShippingSection.cancelChangeAddress}}" stepKey="cancelEditAddress"/> + + <!-- Go to *Next* --> + <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToButtonNext"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="goNext"/> + + <!-- Select payment solution --> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> + + <!--Refresh Page and Place Order--> + <reloadPage stepKey="reloadPage"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + </test> + </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 35e0058440f6e..5335ec2ad775d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -96,14 +96,10 @@ <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check simple product 1 in cart --> @@ -157,14 +153,10 @@ <!-- Check order summary in checkout --> <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="guestCheckoutFillingShippingSection" /> <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="guestCheckoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="{{E2EB2CQuote.subtotal}}"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingTotal" value="{{E2EB2CQuote.shipping}}"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="{{E2EB2CQuote.shippingMethod}}"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="{{E2EB2CQuote.total}}"/> + <argument name="subtotal" value="480.00"/> + <argument name="shippingTotal" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check ship to information in checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 371826c9e7841..65627787e2a05 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -96,14 +96,10 @@ <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check simple product 1 in cart --> @@ -157,14 +153,10 @@ <!-- Check order summary in checkout --> <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="checkoutFillingShippingSection" /> <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="checkoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="{{E2EB2CQuote.subtotal}}"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingTotal" value="{{E2EB2CQuote.shipping}}"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="{{E2EB2CQuote.shippingMethod}}"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="{{E2EB2CQuote.total}}"/> + <argument name="subtotal" value="480.00"/> + <argument name="shippingTotal" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check ship to information in checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml index 9664ec47420cc..89028e146c358 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="IdentityOfDefaultBillingAndShippingAddressTest"> <annotations> <features value="Customer"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml new file mode 100644 index 0000000000000..693c05684f292 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontCheckCustomerInfoCreatedByGuestTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check customer information created by guest"/> + <title value="Check Customer Information Created By Guest"/> + <description value="Check customer information after placing the order as the guest who created an account"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95932"/> + <useCaseId value="MAGETWO-95820"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + + <after> + <deleteData createDataKey="product" stepKey="deleteProduct" /> + <deleteData createDataKey="category" stepKey="deleteCategory" /> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <amOnPage url="$$product.name$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessRegisterSection.orderNumber}}" stepKey="grabOrderNumber"/> + <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="TypePassword"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="TypeConfirmationPassword"/> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> + <see userInput="Thank you for registering" stepKey="verifyAccountCreated"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <amOnPage url="{{AdminOrderPage.url({$grabOrderNumber})}}" stepKey="navigateToOrderPage"/> + <waitForPageLoad stepKey="waitForCreatedOrderPage"/> + <see stepKey="seeCustomerName" userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminShipmentOrderInformationSection.customerName}}"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml new file mode 100644 index 0000000000000..330a026bb9426 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -0,0 +1,97 @@ +<?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="StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest"> + <annotations> + <title value="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"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96537"/> + <useCaseId value="MAGETWO-96431"/> + <group value="Checkout"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> + <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"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + <actionGroup ref="AdminCreateCartPriceRuleWithCouponCode" stepKey="createCartPriceRule"> + <argument name="ruleName" value="CatPriceRule"/> + <argument name="couponCode" value="CatPriceRule.coupon_code"/> + </actionGroup> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStoreFront"> + <argument name="Customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + <amOnPage url="$$simpleProduct.name$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{CatPriceRule.name}}"/> + </actionGroup> + <actionGroup ref="logout" 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"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPage"/> + <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed1"/> + <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 stepKey="chooseFreeShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext1"/> + <waitForPageLoad stepKey="waitForReviewAndPayments1"/> + <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed2"/> + <waitForPageLoad stepKey="waitForCouponTabOpen2"/> + <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{CatPriceRule.coupon_code}}" stepKey="fillCouponCode"/> + <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Your coupon was successfully applied." stepKey="seeSuccessMessage"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> + <waitForPageLoad stepKey="waitForError"/> + <see stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/> + <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + <click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> + <waitForPageLoad stepKey="waitForReviewAndPayments2"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> + <waitForPageLoad stepKey="waitForSuccessfullyPlacedOrder"/> + <see stepKey="seeSuccessMessageForPlacedOrder" userInput="Thank you for your purchase!"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml new file mode 100644 index 0000000000000..fb80b4880a6f4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml @@ -0,0 +1,56 @@ +<?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="StoreFrontUpdateShoppingCartWhileUpdateMinicartTest"> + <annotations> + <stories value="Shopping Cart"/> + <title value="Check updating shopping cart while updating items from minicart"/> + <description value="Check updating shopping cart while updating items from minicart"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-97280"/> + <useCaseId value="MAGETWO-71344"/> + <group value="checkout"/> + </annotations> + + <before> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + </after> + + <!--Add product to cart--> + <amOnPage url="$$createProduct.name$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Go to Shopping cart and check Qty--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCart"/> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyShoppingCart"/> + <assertEquals expected="1" actual="$grabQtyShoppingCart" stepKey="assertQtyShoppingCart"/> + + <!--Open minicart and change Qty--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.quantity}}" stepKey="waitForElementQty"/> + <pressKey selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::BACKSPACE]" stepKey="deleteFiled"/> + <fillField selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" userInput="5" stepKey="changeQty"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="updateQty"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + + <!--Check Qty in shopping cart after updating--> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyShoppingCart1"/> + <assertEquals expected="5" actual="$grabQtyShoppingCart1" stepKey="assertQtyShoppingCart1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml index e7c2ad3dd28a4..fadc9ec50ad8d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml @@ -186,20 +186,20 @@ <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity2"/> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2" /> - <click stepKey="changeShippingAddress" selector="{{CheckoutShippingMethodsSection.shipHereButton}}"/> - <waitForElementNotVisible stepKey="waitForShippingMethodLoaderNotVisible" selector="{{CheckoutShippingMethodsSection.shippingMethodLoader}}" time="30"/> - <waitForElementVisible stepKey="waitForShippingMethodRadioToBeVisible" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" time="30"/> + <click selector="{{CheckoutShippingMethodsSection.shipHereButton}}" stepKey="changeShippingAddress"/> + <waitForElementNotVisible selector="{{CheckoutShippingMethodsSection.shippingMethodLoader}}" time="30" stepKey="waitForShippingMethodLoaderNotVisible"/> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" time="30" stepKey="waitForShippingMethodRadioToBeVisible"/> <waitForPageLoad stepKey="waitForPageLoad23"/> - <click stepKey="selectFirstShippingMethod2" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> - <waitForElement stepKey="waitForShippingMethodSelect2" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> - <click stepKey="clickNextOnShippingMethodLoad2" selector="{{CheckoutShippingMethodsSection.next}}" /> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForShippingMethodSelect2"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNextOnShippingMethodLoad2"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> - <waitForElement stepKey="waitForPlaceOrderButton2" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> - <see stepKey="seeBillingAddressIsCorrect2" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" /> - <click stepKey="clickPlaceOrderButton2" selector="{{CheckoutPaymentSection.placeOrder}}" /> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_NY.street[0]}}" stepKey="seeBillingAddressIsCorrect2" /> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton2"/> <waitForPageLoad stepKey="waitForOrderSuccessPage2"/> - <see stepKey="seeSuccessMessage2" selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" /> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" stepKey="seeSuccessMessage2"/> </test> <test name="StorefrontCustomerCheckoutTestWithRestrictedCountriesForPayment"> <annotations> @@ -216,16 +216,18 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 1" /> - <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry GB" /> + <magentoCLI command="config:set checkout/options/display_billing_address_on 1" stepKey="setShowBillingAddressOnPaymentPage" /> + <magentoCLI command="config:set payment/checkmo/allowspecific 1" stepKey="allowSpecificValue" /> + <magentoCLI command="config:set payment/checkmo/specificcountry GB" stepKey="specificCountryValue" /> <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 0" /> - <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry ''" /> + <magentoCLI command="config:set payment/checkmo/allowspecific 0" stepKey="allowSpecificValue" /> + <magentoCLI command="config:set payment/checkmo/specificcountry ''" stepKey="specificCountryValue" /> + <magentoCLI command="config:set checkout/options/display_billing_address_on 0" stepKey="setDisplayBillingAddressOnPaymentMethod" /> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> </after> <!-- Login as Customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> @@ -253,16 +255,18 @@ <dontsee selector="{{CheckoutPaymentSection.paymentMethodByName('Check / Money order')}}" stepKey="paymentMethodDoesNotAvailable"/> <!-- Fill UK Address and verify that payment available and checkout successful --> - <click selector="{{CheckoutHeaderSection.shippingMethodStep}}" stepKey="goToShipping" /> - <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="fillNewAddress" /> - <actionGroup ref="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup" stepKey="customerCheckoutFillingShippingSectionUK"> - <argument name="customerVar" value="CustomerEntityOne" /> - <argument name="customerAddressVar" value="UK_Not_Default_Address" /> + <uncheckOption selector="{{StorefrontCheckoutPaymentMethodSection.billingAddressSameAsShippingShared}}" stepKey="uncheckBillingAddressSameAsShippingCheckCheckBox"/> + <selectOption selector="{{CheckoutPaymentSection.billingAddressSelectShared}}" userInput="New Address" stepKey="clickOnNewAddress"/> + <waitForPageLoad stepKey="waitNewAddressBillingForm"/> + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> + <argument name="Address" value="updateCustomerUKAddress"/> + <argument name="classPrefix" value="[aria-hidden=false]"/> </actionGroup> + <click selector="{{CheckoutPaymentSection.addressAction('Update')}}" stepKey="clickUpdateBillingAddressButton" /> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="customerSelectCheckMoneyOrderPayment" /> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceorder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> </actionGroup> </test> -</tests> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml index cc5e723c72ea0..651c5bd8d4375 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -71,6 +71,7 @@ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> <!--Refresh Page and Place Order--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <reloadPage stepKey="reloadPage"/> <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> @@ -79,13 +80,17 @@ <!--Verify New addresses in Customer's Address Book--> <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToCustomerAddressBook"/> - <see userInput="{{UK_Not_Default_Address.street[0]}} {{UK_Not_Default_Address.city}}, {{UK_Not_Default_Address.postcode}}" - selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddresses"/> + <see userInput="{{UK_Not_Default_Address.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesStreet"/> + <see userInput="{{UK_Not_Default_Address.city}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesCity"/> + <see userInput="{{UK_Not_Default_Address.postcode}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesPostcode"/> <!--Order review page has address that was created during checkout--> <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="goToOrderReviewPage"/> <see userInput="{{UK_Not_Default_Address.street[0]}} {{UK_Not_Default_Address.city}}, {{UK_Not_Default_Address.postcode}}" selector="{{StorefrontCustomerOrderViewSection.shippingAddress}}" stepKey="checkShippingAddress"/> - <see userInput="{{UK_Not_Default_Address.street[0]}} {{UK_Not_Default_Address.city}}, {{UK_Not_Default_Address.postcode}}" + <see userInput="{{US_Address_TX_Default_Billing.street[0]}}" selector="{{StorefrontCustomerOrderViewSection.billingAddress}}" stepKey="checkBillingAddress"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml index 7b81f12624864..ff61b3be08af1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -63,7 +63,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeAdminOrderStatus"/> - <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Guest" stepKey="seeAdminOrderGuest"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.fullname}}" stepKey="seeAdminOrderGuest"/> <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAdminOrderEmail"/> <see selector="{{AdminOrderDetailsInformationSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderBillingAddress"/> <see selector="{{AdminOrderDetailsInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderShippingAddress"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml new file mode 100644 index 0000000000000..913eb34b34d07 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -0,0 +1,95 @@ +<?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="StorefrontOnePageCheckoutDataWhenChangeQtyTest"> + <annotations> + <stories value="Checkout"/> + <title value="One page Checkout Customer data when changing Product Qty"/> + <description value="One page Checkout Customer data when changing Product Qty"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96960"/> + <useCaseId value="MAGETWO-96850"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create a product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Add product to cart and checkout--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <!--Grab customer data to check it--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone"/> + + <!--Select shipping method and finalize checkout--> + <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForPageLoad stepKey="waitForShippingMethodLoad"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + + <!--Go to cart page, update qty and proceed to checkout--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <see userInput="Shopping Cart" stepKey="seeCartPageIsOpened"/> + <fillField selector="{{CheckoutCartProductSection.qty($$createProduct.name$$)}}" userInput="2" stepKey="updateProductQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCart"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <grabValueFrom selector="{{CheckoutCartProductSection.qty($$createProduct.name$$)}}" stepKey="grabQty"/> + <assertEquals expected="2" actual="$grabQty" stepKey="assertQty"/> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + + <!--Check that form is filled with customer data--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail1"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet1"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity1"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion1"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode1"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone1"/> + + <assertEquals expected="$grabEmail" actual="$grabEmail1" stepKey="assertEmail"/> + <assertEquals expected="$grabFirstName" actual="$grabFirstName1" stepKey="assertFirstName"/> + <assertEquals expected="$grabLastName" actual="$grabLastName1" stepKey="assertLastName"/> + <assertEquals expected="$grabStreet" actual="$grabStreet1" stepKey="assertStreet"/> + <assertEquals expected="$grabCity" actual="$grabCity1" stepKey="assertCity"/> + <assertEquals expected="$grabRegion" actual="$grabRegion1" stepKey="assertRegion"/> + <assertEquals expected="$grabPostcode" actual="$grabPostcode1" stepKey="assertPostcode"/> + <assertEquals expected="$grabTelephone" actual="$grabTelephone1" stepKey="assertTelephone"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml new file mode 100644 index 0000000000000..20ff67a076e1e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.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="StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Persistent Data for Guest Customer with physical quote"/> + <description value="One can use Persistent Data for Guest Customer with physical quote"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13479"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">10</field> + </createData> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + </after> + <!-- 1. Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- 2. Go to Shopping Cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage"/> + <!-- 3. Open "Estimate Shipping and Tax" section and input data --> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection"/> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFlatRateIsPresentInCart"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFreeShippingIsPresentInCart"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <!-- 4. Select Flat Rate as shipping --> + <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearAfterFlatRateSelection"/> + <see selector="{{CheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> + <!-- 5. Refresh browser page (F5) --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> + <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> + <argument name="carrierCode" value="flatrate"/> + <argument name="methodCode" value="flatrate"/> + </actionGroup> + <!-- 6. Go to Checkout --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <actionGroup ref="StorefrontAssertCheckoutEstimateShippingInformationActionGroup" stepKey="assertCheckoutEstimateShippingInformationAfterGoingToCheckout"/> + <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsCheckedAfterGoingToCheckout"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!-- 7. Change persisted data --> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="United Kingdom" stepKey="changeCountryField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="KW1 7NQ" stepKey="changeZipPostalCodeField"/> + <!-- 8. Change shipping rate, select Free Shipping --> + <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="checkFreeShippingAsShippingMethod"/> + <!-- 9. Fill other fields --> + <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> + <!-- 10. Refresh browser page(F5) --> + <reloadPage stepKey="reloadCheckoutPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> + <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <!-- 11. Go back to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage1"/> + <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterGoingBackToShoppingCart"> + <argument name="customerData" value="Simple_UK_Customer_For_Shipment"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsCheckedAfterGoingBackToShoppingCart"> + <argument name="carrierCode" value="freeshipping"/> + <argument name="methodCode" value="freeshipping"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..3401369a8c749 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.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="StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest"> + <annotations> + <features value="Checkout"/> + <title value="Checking Product name in Minicart and on Checkout page with different store views"/> + <description value="Checking Product name in Minicart and on Checkout page with different store views"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96466"/> + <useCaseId value="MAGETWO-96421"/> + <group value="checkout"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create a product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Go to created product page--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToEditPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + + <!--Switch to second store view and change the product name--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + + <!--Switch to second store view--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <waitForPageLoad stepKey="waitForStoreView"/> + + <!--Check product name in Minicart--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <grabTextFrom selector="{{StorefrontMinicartSection.productName}}" stepKey="grabProductNameMinicart"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart1"/> + + <!--Check product name in Shopping Cart page--> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="clickViewAndEdit"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productName}}" stepKey="grabProductNameCart"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameCart" stepKey="assertProductNameCart"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameCart" stepKey="assertProductNameCart1"/> + + <!--Proceed to checkout and check product name in Order Summary area--> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="proceedToCheckout"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + <click selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" stepKey="clickItemInCart"/> + <grabTextFrom selector="{{CheckoutShippingGuestInfoSection.productName}}" stepKey="grabProductNameShipping"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameShipping" stepKey="assertProductNameShipping"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameShipping" stepKey="assertProductNameShipping1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml index 70f950f6f6c35..b0e1dead1fff9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml @@ -19,18 +19,12 @@ <group value="checkout"/> </annotations> <before> - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="defaultVirtualProduct" stepKey="createVirtualProduct"> - <requiredEntity createDataKey="createCategory"/> - </createData> - <createData entity="Simple_US_Customer_CA" stepKey="createCustomer"> - <field key="group_id">1</field> - </createData> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="createCustomer"/> </before> <after> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!-- Steps --> @@ -48,8 +42,8 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingcart"/> <!-- Step 4: Open Estimate Tax section --> <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> - <see selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country_id}}" stepKey="checkCountry"/> - <see selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkState"/> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCountry"/> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkState"/> <scrollTo selector="{{CheckoutCartSummarySection.postcode}}" stepKey="scrollToPostCodeField"/> <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml new file mode 100644 index 0000000000000..423f4049f6722 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml @@ -0,0 +1,65 @@ +<?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="StorefrontUpdateShoppingCartSimpleProductQtyTest"> + <annotations> + <features value="Checkout"/> + <title value="Check updating shopping cart while updating items qty"/> + <description value="Check updating shopping cart while updating items qty"/> + <testCaseId value="MC-14731" /> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add the newly created product to the shopping cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad1"/> + + <!-- Change the product QTY --> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty3Price123.qty}}" stepKey="changeCartQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="openShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad2"/> + + <!-- The price and QTY values should be updated for the product --> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> + <assertEquals stepKey="assertProductQtyInCart"> + <actualResult type="variable">grabProductQtyInCart</actualResult> + <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> + </assertEquals> + + <!-- Subtotal should be updated --> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertCartSubtotal"/> + + <!-- Minicart product price and subtotal should be updated --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> + <assertEquals stepKey="assertProductQtyInMinicart"> + <actualResult type="variable">grabProductQtyInMinicart</actualResult> + <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> + </assertEquals> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml new file mode 100644 index 0000000000000..84080b04c80ee --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml @@ -0,0 +1,70 @@ +<?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="StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest"> + <annotations> + <features value="Checkout"/> + <title value="Check updating shopping cart while updating qty of items with custom options"/> + <description value="Check updating shopping cart while updating qty of items with custom options"/> + <testCaseId value="MC-14732" /> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add two custom options to the product: field and textarea --> + <updateData createDataKey="createProduct" entity="ProductWithTextFieldAndAreaOptions" stepKey="updateProductWithOption"/> + + <!-- Go to the product page, fill the custom options values and add the product to the shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> + <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + + <!-- Change the product QTY --> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty11Subtotal1320.qty}}" stepKey="changeCartQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="updateShoppingCart"/> + <waitForPageLoad stepKey="waitShoppingCartUpdated"/> + + <!-- The price and QTY values should be updated for the product --> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> + <assertEquals stepKey="assertProductQtyInCart"> + <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> + <actualResult type="variable">grabProductQtyInCart</actualResult> + </assertEquals> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> + + <!-- Minicart product price and subtotal should be updated --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> + <assertEquals stepKey="assertProductQtyInMinicart"> + <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> + <actualResult type="variable">grabProductQtyInMinicart</actualResult> + </assertEquals> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml new file mode 100644 index 0000000000000..1b27e1d53adad --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.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="StorefrontValidateEmailOnCheckoutTest"> + <annotations> + <features value="Checkout"/> + <title value="Email validation for Guest on checkout flow"/> + <description value="Email validation for Guest on checkout flow"/> + <stories value="Guest Checkout"/> + <testCaseId value="MC-14695" /> + <group value="checkout"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="SimpleTwo" stepKey="simpleProduct"/> + </before> + + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + </after> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductStorefront"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$" /> + </actionGroup> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage" /> + + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage" /> + <actionGroup ref="AssertStorefrontEmailTooltipContentOnCheckoutActionGroup" stepKey="assertEmailTooltipContent" /> + <actionGroup ref="AssertStorefrontEmailNoteMessageOnCheckoutActionGroup" stepKey="assertEmailNoteMessage" /> + + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailFirstAttempt"> + <argument name="email" value="John" /> + </actionGroup> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageFirstAttempt" /> + + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailSecondAttempt"> + <argument name="email" value="johndoe#example.com" /> + </actionGroup> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageSecondAttempt" /> + + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailThirdAttempt"> + <argument name="email" value="johndoe@example.c" /> + </actionGroup> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageThirdAttempt" /> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml new file mode 100644 index 0000000000000..7318f865a0dc1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.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="UpdateProductFromMiniShoppingCartEntityTest"> + <annotations> + <stories value="Shopping Cart"/> + <title value="Check updating product from mini shopping cart"/> + <description value="Update Product Qty on Mini Shopping Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15068"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!--Create product according to dataset.--> + <createData entity="simpleProductWithoutCategory" stepKey="createProduct"/> + + <!--Add product to cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + </after> + + <actionGroup ref="StorefrontUpdateProductQtyMiniShoppingCartActionGroup" stepKey="updateProductQty"> + <argument name="product" value="$$createProduct$$" /> + <argument name="quote" value="simpleOrderQty2" /> + </actionGroup> + + <!-- Perform all assertions --> + <actionGroup ref="AssertMiniShoppingCartSubTotalActionGroup" stepKey="checkSummary"> + <argument name="dataQuote" value="simpleOrderQty2"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml index 2cc21df85ab67..4b3e18fb31877 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -36,7 +36,6 @@ <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> <createData entity="DisablePaymentMethodsSettingConfig" stepKey="disablePaymentMethodsSettingConfig"/> - <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> <actionGroup ref="logout" stepKey="logout"/> <deleteData createDataKey="simpleproduct" stepKey="deleteProduct"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> @@ -99,5 +98,6 @@ <!--Verify that Created order is in Processing status--> <see selector="{{AdminShipmentOrderInformationSection.orderStatus}}" userInput="Processing" stepKey="seeShipmentOrderStatus"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index 1c5224d007ec8..f69ced3b094c7 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -144,7 +144,8 @@ public function testGetConfig() 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, 'websiteId' => 100, - 'maxItemsToDisplay' => 8 + 'maxItemsToDisplay' => 8, + 'storeId' => null ]; $valueMap = [ @@ -161,7 +162,7 @@ public function testGetConfig() $this->urlBuilderMock->expects($this->exactly(4)) ->method('getUrl') ->willReturnMap($valueMap); - $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); $this->scopeConfigMock->expects($this->at(0)) diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php new file mode 100644 index 0000000000000..23840da97bd47 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Block\Checkout; + +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Helper\Address as AddressHelper; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Checkout\Block\Checkout\AttributeMerger; +use PHPUnit\Framework\TestCase; + +class AttributeMergerTest extends TestCase +{ + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var AddressHelper + */ + private $addressHelper; + + /** + * @var DirectoryHelper + */ + private $directoryHelper; + + /** + * @var AttributeMerger + */ + private $attributeMerger; + + /** + * @inheritdoc + */ + protected function setUp() + { + + $this->customerRepository = $this->createMock(CustomerRepository::class); + $this->customerSession = $this->createMock(CustomerSession::class); + $this->addressHelper = $this->createMock(AddressHelper::class); + $this->directoryHelper = $this->createMock(DirectoryHelper::class); + + $this->attributeMerger = new AttributeMerger( + $this->addressHelper, + $this->customerSession, + $this->customerRepository, + $this->directoryHelper + ); + } + + /** + * Tests of element attributes merging. + * + * @param String $validationRule - validation rule. + * @param String $expectedValidation - expected mapped validation. + * @dataProvider validationRulesDataProvider + */ + public function testMerge(String $validationRule, String $expectedValidation): void + { + $elements = [ + 'field' => [ + 'visible' => true, + 'formElement' => 'input', + 'label' => __('City'), + 'value' => null, + 'sortOrder' => 1, + 'validation' => [ + 'input_validation' => $validationRule + ], + ] + ]; + + $actualResult = $this->attributeMerger->merge( + $elements, + 'provider', + 'dataScope', + ['field' => + [ + 'validation' => ['length' => true] + ] + ] + ); + + $expectedResult = [ + $expectedValidation => true, + 'length' => true + ]; + + self::assertEquals($expectedResult, $actualResult['field']['validation']); + } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'email2'], + ['length', 'validate-length'] + ]; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php index 54f77c95148ac..b54339aa2c1d8 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php @@ -35,7 +35,7 @@ class OnepageTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $serializer; + private $serializerMock; protected function setUp() { @@ -49,7 +49,7 @@ protected function setUp() \Magento\Checkout\Block\Checkout\LayoutProcessorInterface::class ); - $this->serializer = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); $this->model = new \Magento\Checkout\Block\Onepage( $contextMock, @@ -57,7 +57,8 @@ protected function setUp() $this->configProviderMock, [$this->layoutProcessorMock], [], - $this->serializer + $this->serializerMock, + $this->serializerMock ); } @@ -93,6 +94,7 @@ public function testGetJsLayout() $processedLayout = ['layout' => ['processed' => true]]; $jsonLayout = '{"layout":{"processed":true}}'; $this->layoutProcessorMock->expects($this->once())->method('process')->with([])->willReturn($processedLayout); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn($jsonLayout); $this->assertEquals($jsonLayout, $this->model->getJsLayout()); } @@ -101,6 +103,7 @@ public function testGetSerializedCheckoutConfig() { $checkoutConfig = ['checkout', 'config']; $this->configProviderMock->expects($this->once())->method('getConfig')->willReturn($checkoutConfig); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn(json_encode($checkoutConfig)); $this->assertEquals(json_encode($checkoutConfig), $this->model->getSerializedCheckoutConfig()); } diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php index 75e181cbabd08..e3e13cc5b1e69 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php @@ -113,7 +113,7 @@ public function testGetSectionData() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); $productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, @@ -162,6 +162,7 @@ public function testGetSectionData() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } @@ -199,7 +200,7 @@ public function testGetSectionDataWithCompositeProduct() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); $this->checkoutCartMock->expects($this->once())->method('getSummaryQty')->willReturn($summaryQty); $this->checkoutHelperMock->expects($this->once()) @@ -265,6 +266,7 @@ public function testGetSectionDataWithCompositeProduct() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index 853ae0157e64a..1de0ebce10f51 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -273,7 +273,7 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() */ private function getMockForAssignBillingAddress( int $cartId, - \PHPUnit_Framework_MockObject_MockObject $billingAddressMock + \PHPUnit_Framework_MockObject_MockObject $billingAddressMock ) : void { $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); $this->quoteIdMaskFactoryMock->method('create') @@ -287,9 +287,11 @@ private function getMockForAssignBillingAddress( $billingAddressId = 1; $quote = $this->createMock(Quote::class); $quoteBillingAddress = $this->createMock(Address::class); + $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); + $shippingRate->setCarrier('flatrate'); $quoteShippingAddress = $this->createPartialMock( Address::class, - ['setLimitCarrier', 'getShippingMethod'] + ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] ); $this->cartRepositoryMock->method('getActive') ->with($cartId) @@ -309,6 +311,9 @@ private function getMockForAssignBillingAddress( $quote->expects($this->once()) ->method('setBillingAddress') ->with($billingAddressMock); + $quoteShippingAddress->expects($this->any()) + ->method('getShippingRateByCode') + ->willReturn($shippingRate); $quote->expects($this->once()) ->method('setDataChanges') ->willReturnSelf(); diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 71dfd12bb4779..4ebd594a28562 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -49,7 +49,4 @@ </argument> </arguments> </type> - <type name="Magento\Quote\Model\Quote"> - <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> - </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index d80f88786c87b..8f35fe9f37abf 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -59,6 +59,7 @@ <item name="totalsSortOrder" xsi:type="object">Magento\Checkout\Block\Checkout\TotalsProcessor</item> <item name="directoryData" xsi:type="object">Magento\Checkout\Block\Checkout\DirectoryDataProcessor</item> </argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\JsonHexTag</argument> </arguments> </type> <type name="Magento\Checkout\Block\Cart\Totals"> @@ -95,4 +96,7 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> + </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 35733a6119a25..90c2878f501cf 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -46,7 +46,6 @@ </action> <action name="rest/*/V1/guest-carts/*/payment-information"> <section name="cart"/> - <section name="checkout-data"/> </action> <action name="rest/*/V1/guest-carts/*/selected-payment-method"> <section name="cart"/> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index 64b70e80bd84f..a305413bcf1f3 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -105,7 +105,7 @@ <item name="trigger" xsi:type="string">opc-new-shipping-address</item> <item name="buttons" xsi:type="array"> <item name="save" xsi:type="array"> - <item name="text" xsi:type="string" translate="true">Save Address</item> + <item name="text" xsi:type="string" translate="true">Ship here</item> <item name="class" xsi:type="string">action primary action-save-address</item> </item> <item name="cancel" xsi:type="array"> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 1005c11e44d95..84ab9b13d8f3a 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -14,7 +14,8 @@ method="post" id="form-validate" data-mage-init='{"Magento_Checkout/js/action/update-shopping-cart": - {"validationURL" : "/checkout/cart/updateItemQty"} + {"validationURL" : "/checkout/cart/updateItemQty", + "updateCartActionContainer": "#update_cart_action_container"} }' class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml index c1db2f7775ca8..bfb7ddc55cda6 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml @@ -20,6 +20,7 @@ <input type="number" name="qty" id="qty" + min="0" value="" title="<?= /* @escapeNotVerified */ __('Qty') ?>" class="input-text qty" diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index c96df9cdd3195..d15794fb761bb 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -49,7 +49,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima <?php if (isset($_formatedOptionValue['full_view'])): ?> <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> <?php else: ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> <?php endif; ?> </dd> <?php endforeach; ?> @@ -84,20 +84,20 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima <?php endif; ?> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <div class="field qty"> - <label class="label" for="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty"> - <span><?= /* @escapeNotVerified */ __('Qty') ?></span> - </label> <div class="control qty"> - <input id="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty" - name="cart[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" - data-cart-item-id="<?= $block->escapeHtml($_item->getSku()) ?>" - value="<?= /* @escapeNotVerified */ $block->getQty() ?>" - type="number" - size="4" - title="<?= $block->escapeHtml(__('Qty')) ?>" - class="input-text qty" - data-validate="{required:true,'validate-greater-than-zero':true}" - data-role="cart-item-qty"/> + <label for="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty"> + <span class="label"><?= /* @escapeNotVerified */ __('Qty') ?></span> + <input id="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty" + name="cart[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" + data-cart-item-id="<?= $block->escapeHtml($_item->getSku()) ?>" + value="<?= /* @escapeNotVerified */ $block->getQty() ?>" + type="number" + size="4" + title="<?= $block->escapeHtml(__('Qty')) ?>" + class="input-text qty" + data-validate="{required:true,'validate-greater-than-zero':true}" + data-role="cart-item-qty"/> + </label> </div> </div> </td> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml index 1c0c221a550cd..67ac4a9335565 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml @@ -13,3 +13,10 @@ $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> <?= $block->getChildHtml('shopping.cart.table.after') ?> </div> +<script type="text/x-magento-init"> +{ + "*": { + "Magento_Checkout/js/empty-cart": {} + } +} +</script> \ No newline at end of file diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js index 7db0dc5ce7473..c601bb8acf125 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js @@ -12,6 +12,17 @@ define([ 'use strict'; return function (addressData) { - return addressConverter.formAddressDataToQuoteAddress(addressData); + var address = addressConverter.formAddressDataToQuoteAddress(addressData); + + /** + * Returns new customer billing address type. + * + * @returns {String} + */ + address.getType = function () { + return 'new-customer-billing-address'; + }; + + return address; }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js index ce1527b3d72d6..1920bc4d7ac41 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -14,7 +14,8 @@ define([ $.widget('mage.updateShoppingCart', { options: { validationURL: '', - eventName: 'updateCartItemQty' + eventName: 'updateCartItemQty', + updateCartActionContainer: '' }, /** @inheritdoc */ @@ -31,7 +32,9 @@ define([ * @return {Boolean} */ onSubmit: function (event) { - if (!this.options.validationURL) { + var action = this.element.find(this.options.updateCartActionContainer).val(); + + if (!this.options.validationURL || action === 'empty_cart') { return true; } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js index 22b37b2da0b2f..1858ce946fb07 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js @@ -10,7 +10,8 @@ */ define([ 'jquery', - 'Magento_Customer/js/customer-data' + 'Magento_Customer/js/customer-data', + 'jquery/jquery-storageapi' ], function ($, storage) { 'use strict'; @@ -23,6 +24,22 @@ define([ storage.set(cacheKey, data); }, + /** + * @return {*} + */ + initData = function () { + return { + 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage + 'shippingAddressFromData': null, //Shipping address pulled from persistence storage + 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer + 'selectedShippingRate': null, //Shipping rate pulled from persistence storage + 'selectedPaymentMethod': null, //Payment method pulled from persistence storage + 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage + 'billingAddressFromData': null, //Billing address pulled from persistence storage + 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer + }; + }, + /** * @return {*} */ @@ -30,17 +47,12 @@ define([ var data = storage.get(cacheKey)(); if ($.isEmptyObject(data)) { - data = { - 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage - 'shippingAddressFromData': null, //Shipping address pulled from persistence storage - 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer - 'selectedShippingRate': null, //Shipping rate pulled from persistence storage - 'selectedPaymentMethod': null, //Payment method pulled from persistence storage - 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage - 'billingAddressFromData': null, //Billing address pulled from persistence storage - 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer - }; - saveData(data); + data = $.initNamespaceStorage('mage-cache-storage').localStorage.get(cacheKey); + + if ($.isEmptyObject(data)) { + data = initData(); + saveData(data); + } } return data; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js new file mode 100644 index 0000000000000..4b30ad8075274 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js @@ -0,0 +1,16 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Customer/js/customer-data' +], function (customerData) { + 'use strict'; + + var cartData = customerData.get('cart'); + + if (cartData().items && cartData().items.length !== 0) { + customerData.reload(['cart'], false); + } +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index 9cc60a3645d58..bc0ab59b622a2 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -216,11 +216,11 @@ define([ newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); if (selectedBillingAddress) { - if (selectedBillingAddress == 'new-customer-address' && newCustomerBillingAddressData) { //eslint-disable-line + if (selectedBillingAddress === 'new-customer-billing-address' && newCustomerBillingAddressData) { selectBillingAddress(createBillingAddress(newCustomerBillingAddressData)); } else { addressList.some(function (address) { - if (selectedBillingAddress == address.getKey()) { //eslint-disable-line eqeqeq + if (selectedBillingAddress === address.getKey()) { selectBillingAddress(address); } }); @@ -243,7 +243,7 @@ define([ return; } - if (quote.isVirtual()) { + if (quote.isVirtual() || !quote.billingAddress()) { isBillingAddressInitialized = addressList.some(function (addrs) { if (addrs.isDefaultBilling()) { selectBillingAddress(addrs); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js index 1cb35a4cee2db..1337e1affd3d3 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js @@ -35,15 +35,17 @@ define([], function () { * * @returns {Boolean} */ - validate: function () { + validate: function (hideError) { var validationResult = true; + hideError = hideError || false; + if (validators.length <= 0) { return validationResult; } validators.forEach(function (item) { - if (item.validate() == false) { //eslint-disable-line eqeqeq + if (item.validate(hideError) == false) { //eslint-disable-line eqeqeq validationResult = false; return false; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js index c3c5b9d68cec0..c07878fcaea92 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js @@ -9,9 +9,10 @@ define( [ 'mage/storage', 'Magento_Checkout/js/model/error-processor', - 'Magento_Checkout/js/model/full-screen-loader' + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Customer/js/customer-data' ], - function (storage, errorProcessor, fullScreenLoader) { + function (storage, errorProcessor, fullScreenLoader, customerData) { 'use strict'; return function (serviceUrl, payload, messageContainer) { @@ -23,6 +24,23 @@ define( function (response) { errorProcessor.process(response, messageContainer); } + ).success( + function (response) { + var clearData = { + 'selectedShippingAddress': null, + 'shippingAddressFromData': null, + 'newCustomerShippingAddress': null, + 'selectedShippingRate': null, + 'selectedPaymentMethod': null, + 'selectedBillingAddress': null, + 'billingAddressFromData': null, + 'newCustomerBillingAddress': null + }; + + if (response.responseType !== 'error') { + customerData.set('checkout-data', clearData); + } + } ).always( function () { fullScreenLoader.stopLoader(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js index a95471d90dab8..0a5334a42c7e5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js @@ -14,11 +14,13 @@ define([ /** * @param {*} postCode * @param {*} countryId + * @param {Array} postCodesPatterns * @return {Boolean} */ - validate: function (postCode, countryId) { - var patterns = window.checkoutConfig.postCodes[countryId], - pattern, regex; + validate: function (postCode, countryId, postCodesPatterns) { + var pattern, regex, + patterns = postCodesPatterns ? postCodesPatterns[countryId] : + window.checkoutConfig.postCodes[countryId]; this.validatedPostCodeExample = []; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js index 2510d1aced3d3..3486a92736617 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js @@ -7,7 +7,8 @@ */ define([ 'ko', - 'underscore' + 'underscore', + 'domReady!' ], function (ko, _) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index fde88ebadb393..8b07c02e4d380 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -42,6 +42,7 @@ define([ return { validateAddressTimeout: 0, + validateZipCodeTimeout: 0, validateDelay: 2000, /** @@ -133,16 +134,20 @@ define([ }); } else { element.on('value', function () { + clearTimeout(self.validateZipCodeTimeout); + self.validateZipCodeTimeout = setTimeout(function () { + if (element.index === postcodeElementName) { + self.postcodeValidation(element); + } else { + $.each(postcodeElements, function (index, elem) { + self.postcodeValidation(elem); + }); + } + }, delay); + if (!formPopUpState.isVisible()) { clearTimeout(self.validateAddressTimeout); self.validateAddressTimeout = setTimeout(function () { - if (element.index === postcodeElementName) { - self.postcodeValidation(element); - } else { - $.each(postcodeElements, function (index, elem) { - self.postcodeValidation(elem); - }); - } self.validateFields(); }, delay); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index 3ea49cd981d90..eecfa65b189d1 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -14,7 +14,11 @@ define([ _create: function () { var items, i, reload; - $(this.options.emptyCartButton).on('click', $.proxy(function () { + $(this.options.emptyCartButton).on('click', $.proxy(function (event) { + if (event.detail === 0) { + return; + } + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); $(this.options.updateCartActionContainer) .attr('name', 'update_cart_action').attr('value', 'empty_cart'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index dde1ad72ba15e..e66c66006246c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -25,6 +25,7 @@ define([ } }, scrollHeight: 0, + shoppingCartUrl: window.checkout.shoppingCartUrl, /** * Create sidebar. @@ -227,6 +228,10 @@ define([ if (!_.isUndefined(productData)) { $(document).trigger('ajax:updateCartItemQty'); + + if (window.location.href === this.shoppingCartUrl) { + window.location.reload(false); + } } this._hideItemButton(elem); }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 6f9a1a46826da..a552aa01da061 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -40,30 +40,23 @@ function ( 'use strict'; var lastSelectedBillingAddress = null, - newAddressOption = { - /** - * Get new address label - * @returns {String} - */ - getAddressInline: function () { - return $t('New Address'); - }, - customerAddressId: null - }, countryData = customerData.get('directory-data'), addressOptions = addressList().filter(function (address) { - return address.getType() == 'customer-address'; //eslint-disable-line eqeqeq + return address.getType() === 'customer-address'; }); - addressOptions.push(newAddressOption); - return Component.extend({ defaults: { - template: 'Magento_Checkout/billing-address' + template: 'Magento_Checkout/billing-address', + actionsTemplate: 'Magento_Checkout/billing-address/actions', + formTemplate: 'Magento_Checkout/billing-address/form', + detailsTemplate: 'Magento_Checkout/billing-address/details', + links: { + isAddressFormVisible: '${$.billingAddressListProvider}:isNewAddressSelected' + } }, currentBillingAddress: quote.billingAddress, - addressOptions: addressOptions, - customerHasAddresses: addressOptions.length > 1, + customerHasAddresses: addressOptions.length > 0, /** * Init component @@ -84,7 +77,7 @@ function ( .observe({ selectedAddress: null, isAddressDetailsVisible: quote.billingAddress() != null, - isAddressFormVisible: !customer.isLoggedIn() || addressOptions.length === 1, + isAddressFormVisible: !customer.isLoggedIn() || !addressOptions.length, isAddressSameAsShipping: false, saveInAddressBook: 1 }); @@ -147,7 +140,7 @@ function ( updateAddress: function () { var addressData, newBillingAddress; - if (this.selectedAddress() && this.selectedAddress() != newAddressOption) { //eslint-disable-line eqeqeq + if (this.selectedAddress() && !this.isAddressFormVisible()) { selectBillingAddress(this.selectedAddress()); checkoutData.setSelectedBillingAddress(this.selectedAddress().getKey()); } else { @@ -202,6 +195,13 @@ function ( } }, + /** + * Manage cancel button visibility + */ + canUseCancelBillingAddress: ko.computed(function () { + return quote.billingAddress() || lastSelectedBillingAddress; + }), + /** * Restore billing address */ @@ -211,13 +211,6 @@ function ( } }, - /** - * @param {Object} address - */ - onAddressChange: function (address) { - this.isAddressFormVisible(address == newAddressOption); //eslint-disable-line eqeqeq - }, - /** * @param {Number} countryId * @return {*} diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js new file mode 100644 index 0000000000000..ca3a267c01671 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Customer/js/model/address-list', + 'mage/translate', + 'Magento_Customer/js/model/customer' +], function (Component, addressList, $t, customer) { + 'use strict'; + + var newAddressOption = { + /** + * Get new address label + * @returns {String} + */ + getAddressInline: function () { + return $t('New Address'); + }, + customerAddressId: null + }, + addressOptions = addressList().filter(function (address) { + return address.getType() === 'customer-address'; + }); + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/billing-address', + selectedAddress: null, + isNewAddressSelected: false, + addressOptions: addressOptions, + exports: { + selectedAddress: '${ $.parentName }:selectedAddress' + } + }, + + /** + * @returns {Object} Chainable. + */ + initConfig: function () { + this._super(); + this.addressOptions.push(newAddressOption); + + return this; + }, + + /** + * @return {exports.initObservable} + */ + initObservable: function () { + this._super() + .observe('selectedAddress isNewAddressSelected') + .observe({ + isNewAddressSelected: !customer.isLoggedIn() || !addressOptions.length + }); + + return this; + }, + + /** + * @param {Object} address + * @return {*} + */ + addressOptionsText: function (address) { + return address.getAddressInline(); + }, + + /** + * @param {Object} address + */ + onAddressChange: function (address) { + this.isNewAddressSelected(address === newAddressOption); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index a2f8c8c56ff33..5e29fa209a641 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -81,6 +81,7 @@ define([ maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /** * @override */ @@ -101,12 +102,16 @@ define([ self.isLoading(true); }); - if (cartData()['website_id'] !== window.checkout.websiteId) { + if (cartData().website_id !== window.checkout.websiteId || + cartData().store_id !== window.checkout.storeId + ) { customerData.reload(['cart'], false); } return this._super(); }, + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + isLoading: ko.observable(false), initSidebar: initSidebar, diff --git a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html index 406a7d899b67a..5b8dde81dd93e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html @@ -31,15 +31,15 @@ <div class="block block-customer-login" data-bind="attr: {'data-label': $t('or')}"> <div class="block-title"> - <strong id="block-customer-login-heading" - role="heading" - aria-level="2" - data-bind="i18n: 'Sign In'"></strong> + <strong id="block-customer-login-heading-checkout" + role="heading" + aria-level="2" + data-bind="i18n: 'Sign In'"></strong> </div> <!-- ko foreach: getRegion('messages') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> - <div class="block-content" aria-labelledby="block-customer-login-heading"> + <div class="block-content" aria-labelledby="block-customer-login-heading-checkout"> <form data-role="login" data-bind="submit:login" method="post"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html index 5f735fbb4daa9..cabfcc9b3db03 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html @@ -5,28 +5,18 @@ */ --> <div class="checkout-billing-address"> - <div class="billing-address-same-as-shipping-block field choice" data-bind="visible: canUseShippingAddress()"> <input type="checkbox" name="billing-address-same-as-shipping" data-bind="checked: isAddressSameAsShipping, click: useShippingAddress, attr: {id: 'billing-address-same-as-shipping-' + getCode($parent)}"/> <label data-bind="attr: {for: 'billing-address-same-as-shipping-' + getCode($parent)}"><span data-bind="i18n: 'My billing and shipping address are the same'"></span></label> </div> - - <!-- ko template: 'Magento_Checkout/billing-address/details' --><!-- /ko --> + <render args="detailsTemplate"/> <fieldset class="fieldset" data-bind="visible: !isAddressDetailsVisible()"> - <!-- ko template: 'Magento_Checkout/billing-address/list' --><!-- /ko --> - <!-- ko template: 'Magento_Checkout/billing-address/form' --><!-- /ko --> - <div class="actions-toolbar"> - <div class="primary"> - <button class="action action-update" type="button" data-bind="click: updateAddress"> - <span data-bind="i18n: 'Update'"></span> - </button> - <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> - <span data-bind="i18n: 'Cancel'"></span> - </button> - </div> + <each args="getRegion('billing-address-list')" render="" /> + <div data-bind="fadeVisible: isAddressFormVisible"> + <render args="formTemplate"/> </div> + <render args="actionsTemplate"/> </fieldset> - </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html new file mode 100644 index 0000000000000..860f340d3f7ca --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html @@ -0,0 +1,21 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="actions-toolbar"> + <div class="primary"> + <button class="action action-update" + type="button" + click="updateAddress"> + <span translate="'Update'"/> + </button> + <button class="action action-cancel" + type="button" + click="cancelAddressEdit" + visible="canUseCancelBillingAddress()"> + <span translate="'Cancel'"/> + </button> + </div> +</div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html index 54fe9a1f59394..e29ed99d17be1 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ --> -<div class="billing-address-form" data-bind="fadeVisible: isAddressFormVisible"> +<div class="billing-address-form"> <!-- ko foreach: getRegion('before-fields') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html index 8d6142e07fcf0..6a784fa7a04c4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html @@ -14,7 +14,7 @@ method="post"> <fieldset id="customer-email-fieldset" class="fieldset" data-bind="blockLoader: isLoading"> <div class="field required"> - <label class="label" for="customer-email"> + <label class="label" for="checkout-customer-email"> <span data-bind="i18n: 'Email Address'"></span> </label> <div class="control _with-tooltip"> @@ -26,7 +26,7 @@ mageInit: {'mage/trim-input':{}}" name="username" data-validate="{required:true, 'validate-email':true}" - id="customer-email" /> + id="checkout-customer-email" /> <!-- ko template: 'ui/form/element/helper/tooltip' --><!-- /ko --> <span class="note" data-bind="fadeVisible: isPasswordVisible() == false"><!-- ko i18n: 'You can create an account after checkout.'--><!-- /ko --></span> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index 2daca51a2f5da..fb128a891aea2 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -97,7 +97,7 @@ </div> </div> - <div id="minicart-widgets" class="minicart-widgets"> + <div id="minicart-widgets" class="minicart-widgets" if="getRegion('promotion').length"> <each args="getRegion('promotion')" render=""/> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 357b0e550af0f..41d442a76d510 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -45,7 +45,7 @@ <span data-bind="html: option.value.join('<br>')"></span> <!-- /ko --> <!-- ko ifnot: Array.isArray(option.value) --> - <span data-bind="html: option.value"></span> + <span data-bind="text: option.value"></span> <!-- /ko --> </dd> <!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html index a1a5aa67a9688..1fcfa4b3b1343 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html @@ -16,12 +16,14 @@ <!-- Address form pop up --> <if args="!isFormInline"> - <button type="button" - class="action action-show-popup" - click="showFormPopUp" - visible="!isNewAddressAdded()"> - <span translate="'New Address'" /> - </button> + <div class="new-address-popup"> + <button type="button" + class="action action-show-popup" + click="showFormPopUp" + visible="!isNewAddressAdded()"> + <span translate="'New Address'" /> + </button> + </div> <div id="opc-new-shipping-address" visible="isFormPopUpVisible()" render="shippingFormTemplate" /> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 34ec91aa43c72..fc74a4691a2e7 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -6,7 +6,7 @@ --> <div class="block items-in-cart" data-bind="mageInit: {'collapsible':{'openedState': 'active', 'active': isItemsBlockExpanded()}}"> <div class="title" data-role="title"> - <strong role="heading"> + <strong role="heading" aria-level="1"> <translate args="maxCartItemsToDisplay" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> <translate args="'of'" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> <span data-bind="text: getCartLineItemsCount()"></span> diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 1f1b5be9683ed..1217270d780e1 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -67,17 +67,18 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { $agreements = []; $agreements['checkoutAgreements'] = $this->getAgreementsConfig(); + return $agreements; } /** - * Returns agreements config + * Returns agreements config. * * @return array */ @@ -99,7 +100,7 @@ protected function getAgreementsConfig() 'content' => $agreement->getIsHtml() ? $agreement->getContent() : nl2br($this->escaper->escapeHtml($agreement->getContent())), - 'checkboxText' => $agreement->getCheckboxText(), + 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), 'agreementId' => $agreement->getAgreementId() ]; diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml new file mode 100644 index 0000000000000..d42c2c8139425 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.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="AdminMenuStoresSettingsTermsAndConditions"> + <data key="pageTitle">Terms and Conditions</data> + <data key="title">Terms and Conditions</data> + <data key="dataUiId">magento-checkoutagreements-sales-checkoutagreement</data> + </entity> +</entities> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml new file mode 100644 index 0000000000000..d2d4cb9138bd5 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresTermsAndConditionsNavigateMenuTest"> + <annotations> + <features value="CheckoutAgreements"/> + <stories value="Menu Navigation"/> + <title value="Admin stores terms and conditions navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Terms and Conditions"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14148"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTermsAndConditionsPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsTermsAndConditions.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsTermsAndConditions.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index c59a3d2433ec2..c8309bacb0a86 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -8,6 +8,9 @@ use Magento\CheckoutAgreements\Model\AgreementsProvider; use Magento\Store\Model\ScopeInterface; +/** + * Tests for AgreementsConfigProvider. + */ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase { /** @@ -35,6 +38,9 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase */ private $agreementsFilterMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -59,10 +65,16 @@ protected function setUp() ); } + /** + * Test for getConfig if content is HTML. + * + * @return void + */ public function testGetConfigIfContentIsHtml() { $content = 'content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -71,12 +83,12 @@ public function testGetConfigIfContentIsHtml() 'agreements' => [ [ 'content' => $content, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -94,6 +106,11 @@ public function testGetConfigIfContentIsHtml() ->with($searchCriteriaMock) ->willReturn([$agreement]); + $this->escaperMock->expects($this->once()) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); + $agreement->expects($this->once())->method('getIsHtml')->willReturn(true); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); @@ -103,11 +120,17 @@ public function testGetConfigIfContentIsHtml() $this->assertEquals($expectedResult, $this->model->getConfig()); } + /** + * Test for getConfig if content is not HTML. + * + * @return void + */ public function testGetConfigIfContentIsNotHtml() { $content = 'content'; $escapedContent = 'escaped_content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -116,12 +139,12 @@ public function testGetConfigIfContentIsNotHtml() 'agreements' => [ [ 'content' => $escapedContent, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -139,8 +162,11 @@ public function testGetConfigIfContentIsNotHtml() ->with($searchCriteriaMock) ->willReturn([$agreement]); - $this->escaperMock->expects($this->once())->method('escapeHtml')->with($content)->willReturn($escapedContent); - + $this->escaperMock->expects($this->at(0))->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(false); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); diff --git a/app/code/Magento/CheckoutAgreements/etc/di.xml b/app/code/Magento/CheckoutAgreements/etc/di.xml index 081e3daa781ff..a8ff8f5941f96 100644 --- a/app/code/Magento/CheckoutAgreements/etc/di.xml +++ b/app/code/Magento/CheckoutAgreements/etc/di.xml @@ -23,7 +23,7 @@ <type name="Magento\Checkout\Api\GuestPaymentInformationManagementInterface"> <plugin name="validate-guest-agreements" type="Magento\CheckoutAgreements\Model\Checkout\Plugin\GuestValidation"/> </type> - <type name="\Magento\CheckoutAgreements\Model\CheckoutAgreementsList"> + <type name="Magento\CheckoutAgreements\Model\CheckoutAgreementsList"> <arguments> <argument name="collectionProcessor" xsi:type="object">Magento\CheckoutAgreements\Model\Api\SearchCriteria\CollectionProcessor</argument> </arguments> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml index 3f742de0177da..122160f1a10cd 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout_overview"> - <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::multishipping_agreements.phtml"/> + <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::additional_agreements.phtml"/> </referenceBlock> </body> </page> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index 3400770f5cee8..33227f0cdce3c 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js index 157923323fd0e..cbd06b51fe1b5 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js @@ -19,7 +19,7 @@ define([ * * @returns {Boolean} */ - validate: function () { + validate: function (hideError) { var isValid = true; if (!agreementsConfig.isEnabled || $(agreementsInputPath).length === 0) { @@ -28,7 +28,8 @@ define([ $(agreementsInputPath).each(function (index, element) { if (!$.validator.validateSingleElement(element, { - errorElement: 'div' + errorElement: 'div', + hideError: hideError || false })) { isValid = false; } diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index a448537d64e83..4b1a68624e547 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -5,17 +5,17 @@ */ --> <div data-role="checkout-agreements"> - <div class="checkout-agreements" data-bind="visible: isVisible"> + <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> <!-- ko foreach: agreements --> <!-- ko if: ($parent.isAgreementRequired($data)) --> - <div class="checkout-agreement required"> + <div class="checkout-agreement field choice required"> <input type="checkbox" class="required-entry" data-bind="attr: { 'id': $parent.getCheckboxId($parentContext, agreementId), 'name': 'agreement[' + agreementId + ']', 'value': agreementId }"/> - <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> + <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> <button type="button" class="action action-show" data-bind="click: function(data, event) { return $parent.showContent(data, event) }" diff --git a/app/code/Magento/Cms/Block/Widget/Block.php b/app/code/Magento/Cms/Block/Widget/Block.php index aa6aeaff4ecbe..c665f2afc5d38 100644 --- a/app/code/Magento/Cms/Block/Widget/Block.php +++ b/app/code/Magento/Cms/Block/Widget/Block.php @@ -83,7 +83,7 @@ protected function _beforeToHtml() if ($block && $block->isActive()) { try { - $storeId = $this->_storeManager->getStore()->getId(); + $storeId = $this->getData('store_id') ?? $this->_storeManager->getStore()->getId(); $this->setText( $this->_filterProvider->getBlockFilter()->setStoreId($storeId)->filter($block->getContent()) ); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index a7f49e8a431a4..82d200beb6dc9 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -6,12 +6,13 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** * Creates new folder. */ -class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 31b01ce115c21..9bad371aa84d7 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -4,6 +4,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; @@ -58,13 +61,20 @@ public function execute() __('Directory %1 is not under storage root path.', $path) ); } - $result = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); + $uploaded = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); + $response = [ + 'name' => $uploaded['name'], + 'type' => $uploaded['type'], + 'error' => $uploaded['error'], + 'size' => $uploaded['size'], + 'file' => $uploaded['file'] + ]; } catch (\Exception $e) { - $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; + $response = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData($result); + return $resultJson->setData($response); } } diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php index abd260b260b93..70e9437235ac3 100644 --- a/app/code/Magento/Cms/Helper/Page.php +++ b/app/code/Magento/Cms/Helper/Page.php @@ -187,7 +187,7 @@ public function getPageUrl($pageId = null) { /** @var \Magento\Cms\Model\Page $page */ $page = $this->_pageFactory->create(); - if ($pageId !== null && $pageId !== $page->getId()) { + if ($pageId !== null) { $page->setStoreId($this->_storeManager->getStore()->getId()); $page->load($pageId); } diff --git a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php index fb759348759b2..23a452c0fe58c 100644 --- a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php +++ b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php @@ -20,6 +20,7 @@ class PageLayout implements OptionSourceInterface /** * @var array + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $options; @@ -34,16 +35,10 @@ public function __construct(BuilderInterface $pageLayoutBuilder) } /** - * Get options - * - * @return array + * @inheritdoc */ public function toOptionArray() { - if ($this->options !== null) { - return $this->options; - } - $configOptions = $this->pageLayoutBuilder->getPageLayoutsConfig()->getOptions(); $options = []; foreach ($configOptions as $key => $value) { @@ -54,6 +49,6 @@ public function toOptionArray() } $this->options = $options; - return $this->options; + return $options; } } diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block.php b/app/code/Magento/Cms/Model/ResourceModel/Block.php index 9aab54b02bc14..30e817713755c 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block.php @@ -95,9 +95,11 @@ protected function _beforeSave(AbstractModel $object) } /** + * Get block id. + * * @param AbstractModel $object * @param mixed $value - * @param null $field + * @param string $field * @return bool|int|string * @throws LocalizedException * @throws \Exception @@ -183,10 +185,12 @@ public function getIsUniqueBlockToStores(AbstractModel $object) $entityMetadata = $this->metadataPool->getMetadata(BlockInterface::class); $linkField = $entityMetadata->getLinkField(); - if ($this->_storeManager->isSingleStoreMode()) { - $stores = [Store::DEFAULT_STORE_ID]; - } else { - $stores = (array)$object->getData('store_id'); + $stores = (array)$object->getData('store_id'); + $isDefaultStore = $this->_storeManager->isSingleStoreMode() + || array_search(Store::DEFAULT_STORE_ID, $stores) !== false; + + if (!$isDefaultStore) { + $stores[] = Store::DEFAULT_STORE_ID; } $select = $this->getConnection()->select() @@ -196,8 +200,11 @@ public function getIsUniqueBlockToStores(AbstractModel $object) 'cb.' . $linkField . ' = cbs.' . $linkField, [] ) - ->where('cb.identifier = ?', $object->getData('identifier')) - ->where('cbs.store_id IN (?)', $stores); + ->where('cb.identifier = ? ', $object->getData('identifier')); + + if (!$isDefaultStore) { + $select->where('cbs.store_id IN (?)', $stores); + } if ($object->getId()) { $select->where('cb.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId()); @@ -236,6 +243,8 @@ public function lookupStoreIds($id) } /** + * Save an object. + * * @param AbstractModel $object * @return $this * @throws \Exception diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index b2ef78bab9909..dfbbce99b6515 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -3,17 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Model\Wysiwyg\Images; use Magento\Cms\Helper\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; /** - * Wysiwyg Images model + * Wysiwyg Images model. + * + * Tightly connected with controllers responsible for managing files so it uses session and is (sort of) a part + * of the presentation layer. * * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * * @api * @since 100.0.2 @@ -270,7 +277,8 @@ public function getDirsCollection($path) $collection = $this->getCollection($path) ->setCollectDirs(true) ->setCollectFiles(false) - ->setCollectRecursively(false); + ->setCollectRecursively(false) + ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); $conditions = $this->getConditionsForExcludeDirs(); diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml index 58318660d2c42..dde6237390257 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml @@ -23,4 +23,14 @@ <executeJS function="(el = document.querySelector('[name=\'identifier\']')) && el['se' + 'tAt' + 'tribute']('data-value', el.value.split('-')[0]);" stepKey="setAttribute" /> <seeElement selector="{{CmsNewPagePageBasicFieldsSection.duplicatedURLKey(_duplicatedCMSPage.title)}}" stepKey="see"/> </actionGroup> + <actionGroup name="AssertStoreFrontCMSPage"> + <arguments> + <argument name="cmsTitle" type="string"/> + <argument name="cmsContent" type="string"/> + <argument name="cmsContentHeading" type="string"/> + </arguments> + <see selector="{{StorefrontCMSPageSection.title}}" userInput="{{cmsTitle}}" stepKey="seeTitle"/> + <see selector="{{StorefrontCMSPageSection.mainTitle}}" userInput="{{cmsContentHeading}}" stepKey="seeContentHeading"/> + <see selector="{{StorefrontCMSPageSection.mainContent}}" userInput="{{cmsContent}}" stepKey="seeContent"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml index 75e059f620c2d..07e43347d9ddd 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml @@ -44,4 +44,75 @@ <waitForPageLoad stepKey="waitForPageLoad3"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskOfStagingSection" /> </actionGroup> + <actionGroup name="DeleteCMSBlockActionGroup"> + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{CmsPagesPageActionsSection.select(_defaultBlock.title)}}" stepKey="ClickOnSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(_defaultBlock.title)}}" stepKey="ClickOnEdit"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="ClickToConfirm"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <see userInput="You deleted the block." stepKey="VerifyBlockIsDeleted"/> + </actionGroup> + <actionGroup name="AddStoreViewToCmsPage" extends="navigateToCreatedCMSPage"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <remove keyForRemoval="clickExpandContentTabForPage"/> + <remove keyForRemoval="waitForLoadingMaskOfStagingSection"/> + <click selector="{{CmsNewPagePiwSection.header}}" stepKey="clickPageInWebsites" after="waitForPageLoad3"/> + <waitForElementVisible selector="{{CmsNewPagePiwSection.selectStoreView(storeViewName)}}" stepKey="waitForStoreGridReload"/> + <clickWithLeftButton selector="{{CmsNewPagePiwSection.selectStoreView(storeViewName)}}" stepKey="clickStoreView"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You saved the page." stepKey="seeMessage"/> + </actionGroup> + <actionGroup name="saveAndCloseCMSBlockWithSplitButton"> + <waitForElementVisible selector="{{BlockNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForExpandSplitButtonToBeVisible" /> + <click selector="{{BlockNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitButton"/> + <click selector="{{BlockNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveBlock"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClickingSave" /> + <see userInput="You saved the block." stepKey="assertSaveBlockSuccessMessage"/> + </actionGroup> + <actionGroup name="navigateToStorefrontForCreatedPage"> + <arguments> + <argument name="page" type="string"/> + </arguments> + <amOnPage url="{{page}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <actionGroup name="saveCMSBlock"> + <waitForElementVisible selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="waitForSaveButton"/> + <click selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You saved the block." stepKey="seeSuccessfulSaveMessage"/> + </actionGroup> + <actionGroup name="saveAndContinueEditCmsPage"> + <waitForElementVisible time="10" selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForSaveAndContinueVisibility"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSaveAndContinueEditCmsPage"/> + <waitForPageLoad stepKey="waitForCmsPageLoad"/> + <waitForElementVisible time="1" selector="{{CmsNewPagePageActionsSection.cmsPageTitle}}" stepKey="waitForCmsPageSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + </actionGroup> + <actionGroup name="saveCmsPage"> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForSplitButton"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitButton"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="waitForSaveCmsPage"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSaveCmsPage"/> + <waitForElementVisible time="1" selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="waitForCmsPageSaveButton"/> + <see userInput="You saved the page." selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="assertSavePageSuccessMessage"/> + </actionGroup> + <actionGroup name="setLayout"> + <arguments> + <argument name="designSection"/> + <argument name="layoutOption"/> + </arguments> + <waitForElementVisible selector="{{designSection.DesignTab}}" stepKey="waitForDesignTabVisible"/> + <conditionalClick selector="{{designSection.DesignTab}}" dependentSelector="{{designSection.LayoutDropdown}}" visible="false" stepKey="clickOnDesignTab"/> + <waitForPageLoad stepKey="waitForPageLoadDesignTab"/> + <waitForElementVisible selector="{{designSection.LayoutDropdown}}" stepKey="waitForLayoutDropDown" /> + <selectOption selector="{{designSection.LayoutDropdown}}" userInput="{{layoutOption}}" stepKey="selectLayout"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml new file mode 100644 index 0000000000000..2fa1b86a61572 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml @@ -0,0 +1,24 @@ +<?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="ClearWidgetsFromCMSContent"> + <amOnPage url="{{CmsPageEditPage.url('2')}}" stepKey="navigateToEditHomePagePage"/> + <waitForPageLoad stepKey="waitEditHomePagePageToLoad"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> + <waitForElementNotVisible selector="{{CmsWYSIWYGSection.CheckIfTabExpand}}" stepKey="waitForTabExpand"/> + <executeJS function="jQuery('[id=\'cms_page_form_content_ifr\']').attr('name', 'preview-iframe')" stepKey="setPreviewFrameName"/> + <switchToIFrame selector="preview-iframe" stepKey="switchToIframe"/> + <fillField selector="{{TinyMCESection.EditorContent}}" userInput="Hello TinyMCE4!" stepKey="clearWidgets"/> + <switchToIFrame stepKey="switchOutFromIframe"/> + <executeJS function="tinyMCE.activeEditor.setContent('Hello TinyMCE4!');" stepKey="executeJSFillContent1"/> + <click selector="{{InsertWidgetSection.save}}" stepKey="saveWidget"/> + <waitForPageLoad stepKey="waitSaveToBeApplied"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the page." stepKey="seeSaveSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml new file mode 100644 index 0000000000000..a4b88c544de88 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml @@ -0,0 +1,40 @@ +<?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="CreateNewPageWithWidget"> + <arguments> + <argument name="pageTitle" type="string" defaultValue="{{defaultCmsPage.title}}"/> + <argument name="category" type="string"/> + <argument name="condition" type="string"/> + <argument name="widgetType" type="string"/> + </arguments> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <click selector="{{CmsNewPagePageActionsSection.insertWidget}}" stepKey="clickToInsertWidget"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForElementVisible stepKey="waitForInsertWidgetTitle" selector="{{WidgetSection.InsertWidgetTitle}}"/> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="{{widgetType}}" stepKey="selectCatalogProductsList"/> + <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParam"/> + <scrollTo selector="{{WidgetSection.AddParam}}" stepKey="scrollToAddParamElement"/> + <click selector="{{WidgetSection.AddParam}}" stepKey="addParam"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="{{condition}}" stepKey="selectCategory"/> + <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParam"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickToAddRuleParam"/> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickToSelectFromList"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{WidgetSection.PreCreateCategory(category)}}" stepKey="selectPreCategory" /> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickToSaveInsertedWidget"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="saveCMSPage"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="waitForSuccessMessageLoggedOut" time="5"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..3e227df56c909 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,26 @@ +<?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="AdminMenuContent"> + <data key="pageTitle">Content</data> + <data key="title">Content</data> + <data key="dataUiId">magento-backend-content</data> + </entity> + <entity name="AdminMenuContentElementsPages"> + <data key="pageTitle">Pages</data> + <data key="title">Pages</data> + <data key="dataUiId">magento-cms-cms-page</data> + </entity> + <entity name="AdminMenuContentElementsBlocks"> + <data key="pageTitle">Blocks</data> + <data key="title">Blocks</data> + <data key="dataUiId">magento-cms-cms-block</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml new file mode 100644 index 0000000000000..dea047ec43568 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -0,0 +1,18 @@ +<?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="Sales25offBlock" type="block"> + <data key="title" unique="suffix">Sales25off</data> + <data key="identifier" unique="suffix">Sales25off</data> + <data key="store_id">All Store Views</data> + <data key="content">sales25off everything!</data> + <data key="is_active">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml index d8a8401c31f15..2f8efac37cecf 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml @@ -50,6 +50,7 @@ <data key="file_type">Upload File</data> <data key="shareable">Yes</data> <data key="file">magento-again.jpg</data> + <data key="fileName">magento-again</data> <data key="value">magento-again.jpg</data> <data key="content">Image content. Yeah.</data> <data key="height">1000</data> @@ -71,6 +72,7 @@ <data key="file_type">Upload File</data> <data key="shareable">Yes</data> <data key="value">magento3.jpg</data> + <data key="file">magento3.jpg</data> <data key="fileName">magento3</data> <data key="extension">jpg</data> <data key="content">Image content. Yeah.</data> @@ -86,4 +88,13 @@ <data key="content">1<br/>2<br/>3<br/>4<br/>5<br/>6<br/>7<br/>8<br/>9<br/>10<br/>11<br/>12<br/>13<br/>14<br/>15<br/>16<br/>17<br/>18<br/>19<br/>20<br/>line21<br/>22<br/>23<br/>24<br/>25<br/>26<br/>line27<br/>2<br/>3<br/>4<br/>5</data> <data key="identifier" unique="suffix">test-page-</data> </entity> + <entity name="_emptyCmsPage" type="cms_page"> + <data key="title" unique="suffix">Test CMS Page</data> + <data key="identifier" unique="suffix">test-page-</data> + </entity> + <entity name="_emptyCmsBlock" type="block"> + <data key="title" unique="suffix">Test CMS Block</data> + <data key="identifier" unique="suffix" >block</data> + <data key="active">true</data> + </entity> </entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml new file mode 100644 index 0000000000000..61dfb051d101e --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml @@ -0,0 +1,14 @@ +<?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="defaultCmsPage" type="block"> + <data key="title" unique="suffix">CMSpage</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml new file mode 100644 index 0000000000000..3fd100ee02aa2 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditBlockPage" url="cms/block/edit/block_id" area="admin" module="Magento_Cms"> + <section name="AdminUpdateBlockSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml new file mode 100644 index 0000000000000..73db6b61343b1 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CmsPageEditPage" area="admin" url="admin/cms_page/edit/page_id/{{var}}" parameterized="true" module="Magento_Cms"> + <section name="CmsNewPagePageActionsSection"/> + <section name="CmsNewPagePageBasicFieldsSection"/> + <section name="CmsNewPagePageContentSection"/> + <section name="CmsNewPagePageSeoSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml new file mode 100644 index 0000000000000..ab15570a01f40 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminBlockGridSection"> + <element name="search" type="input" selector="//input[@placeholder='Search by keyword']"/> + <element name="searchButton" type="button" selector="//div[@class='data-grid-search-control-wrap']//label[@class='data-grid-search-label']/following-sibling::button[@class='action-submit']"/> + <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> + <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> + <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml index 4c6014af51264..2efa7f62fc4ec 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml @@ -17,6 +17,7 @@ <element name="saveAndDuplicate" type="button" selector="#save_and_duplicate" timeout="10"/> <element name="saveAndClose" type="button" selector="#save_and_close" timeout="10"/> <element name="expandSplitButton" type="button" selector="//button[@data-ui-id='save-button-dropdown']" timeout="10"/> + <element name="back" type="button" selector="#back"/> </section> <section name="BlockWYSIWYGSection"> <element name="ShowHideBtn" type="button" selector="#togglecms_block_form_content"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml index 42f8f4d00ee9f..a340d0af1e7a1 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml @@ -22,5 +22,6 @@ <element name="content" type="input" selector="//textarea[@name='content']"/> <element name="spinner" type="input" selector='//div[@data-component="cms_page_form.cms_page_form"]' /> <element name="saveAndClose" type="button" selector="#save_and_close" timeout="10"/> + <element name="insertWidget" type="button" selector="//span[contains(text(),'Insert Widget...')]"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml index 280c7dfd8263e..4ce8842c1ad87 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml @@ -14,5 +14,6 @@ <element name="mainTitle" type="text" selector="#maincontent .page-title"/> <element name="mainContent" type="text" selector="#maincontent"/> <element name="footerTop" type="text" selector="footer.page-footer"/> + <element name="title" type="text" selector="//div[@class='breadcrumbs']//ul/li[@class='item cms_page']"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml deleted file mode 100644 index d26f7d83616d5..0000000000000 --- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?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="StorefrontHeaderSection"> - </section> -</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml index 89431a0c7ce4d..ff6167ffc10e0 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml @@ -31,6 +31,8 @@ <element name="InsertImage" type="button" selector=".mce-i-image" /> <element name="InsertTable" type="button" selector=".mce-i-table" /> <element name="SpecialCharacter" type="button" selector=".mce-i-charmap" /> + <element name="WidgetButton" type="button" selector="span[class*='magento-widget mceNonEditable']"/> + <element name="EditorContent" type="input" selector="#tinymce"/> </section> <section name="MediaGallerySection"> <element name="Browse" type="button" selector=".mce-i-browse"/> @@ -99,6 +101,7 @@ <element name="AddParam" type="button" selector=".rule-param-add"/> <element name="ConditionsDropdown" type="select" selector="#conditions__1__new_child"/> <element name="RuleParam" type="button" selector="//a[text()='...']"/> + <element name="RuleParam1" type="button" selector="(//span[@class='rule-param']//a)[{{var}}]" parameterized="true"/> <element name="RuleParamSelect" type="select" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//select" parameterized="true"/> <element name="RuleParamInput" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//input" parameterized="true"/> <element name="RuleParamLabel" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//a" parameterized="true"/> @@ -111,5 +114,8 @@ <element name="CompareBtn" type="button" selector=".action.tocompare"/> <element name="ClearCompare" type="button" selector="#compare-clear-all"/> <element name="AcceptClear" type="button" selector=".action-primary.action-accept" /> + <element name="ChooserName" type="input" selector="input[name='chooser_name']" /> + <element name="SelectPageButton" type="button" selector="//button[@title='Select Page...']"/> + <element name="SelectPageFilterInput" type="input" selector="input.admin__control-text[name='{{filterName}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml index ded94eab92042..1adb781a67536 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml @@ -36,7 +36,7 @@ <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="CMS Page Link" stepKey="selectCMSPageLink" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml index 5b3679bed77e0..393e25e474f12 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml @@ -46,7 +46,9 @@ <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Category Link Block Template" stepKey="selectTemplate" /> <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectCategoryBtn" /> <waitForLoadingMaskToDisappear stepKey="wait3"/> - <click userInput="$$createPreReqCategory.name$$" stepKey="selectPreCreateCategory" /> + <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory}}" stepKey="expandRootCategory" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="expandWait" /> + <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCreateCategory" /> <waitForElementNotVisible selector="{{WidgetSection.SelectCategoryTitle}}" stepKey="waitForSlideoutCloses1" /> <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> <waitForElementNotVisible selector="{{WidgetSection.InsertWidgetTitle}}" stepKey="waitForSlideOutCloses2" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml index 123d25f92b6b7..9ee9d27de477a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml @@ -50,6 +50,8 @@ <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Product Link Block Template" stepKey="selectTemplate" /> <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectPageBtn" /> <waitForLoadingMaskToDisappear stepKey="wait4"/> + <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory}}" stepKey="expandRootCategory" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="expandWait" /> <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCategory" /> <waitForLoadingMaskToDisappear stepKey="waitLoadingMask" /> <click selector="{{WidgetSection.PreCreateProduct('$$createPreReqProduct.name$$')}}" stepKey="selectPreProduct" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml index 2586ffc11d086..394d79bda1ab3 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml @@ -42,7 +42,7 @@ <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml index 691a99a73b90b..862f51ea72fad 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml @@ -41,7 +41,7 @@ <waitForPageLoad stepKey="wait2"/> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Compared Products" stepKey="selectRecentlyComparedProducts" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml index 9cdbccd1f8c32..298aed917fc18 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml @@ -40,7 +40,7 @@ <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Viewed Products" stepKey="selectRecentlyViewedProducts" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml new file mode 100644 index 0000000000000..19f501d6aa209 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminContentBlocksNavigateMenuTest"> + <annotations> + <features value="Cms"/> + <stories value="Menu Navigation"/> + <title value="Admin content blocks navigate menu test"/> + <description value="Admin should be able to navigate to Content > Blocks"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14129"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentBlocksPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsBlocks.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentElementsBlocks.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml new file mode 100644 index 0000000000000..323a1de7b9a4e --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminContentPagesNavigateMenuTest"> + <annotations> + <features value="Cms"/> + <stories value="Menu Navigation"/> + <title value="Admin content pages navigate menu test"/> + <description value="Admin should be able to navigate to Content > Pages"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14128"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentPagesPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsPages.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentElementsPages.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml new file mode 100644 index 0000000000000..e6ab1c130606b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -0,0 +1,74 @@ +<?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="CheckStaticBlocksTest"> + <annotations> + <features value="Cms"/> + <stories value="MAGETWO-91559 - Static blocks with same ID appear in place of correct block"/> + <title value="Check static blocks: ID should be unique per Store View"/> + <description value="Check static blocks: ID should be unique per Store View"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94229"/> + <group value="Cms"/> + </annotations> + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + + <!--Go to Cms blocks page--> + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <seeInCurrentUrl url="cms/block/" stepKey="VerifyPageIsOpened"/> + <!--Click to create new block--> + <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened"/> + <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent"/> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <see userInput="You saved the block." stepKey="VerifyBlockIsSaved"/> + <!--Click to go back and add new block--> + <click selector="{{BlockNewPagePageActionsSection.back}}" stepKey="ClickToGoBack"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock1"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened1"/> + <!--Add new BLock with the same data--> + <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent1"/> + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="Default Store View" stepKey="selectDefaultStoreView" /> + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="{{customStore.name}}" stepKey="selectSecondStoreView1" /> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock1"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <!--Verify that corresponding message is displayed--> + <see userInput="A block identifier with the same properties already exists in the selected store." stepKey="VerifyBlockIsSaved1"/> + + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml new file mode 100644 index 0000000000000..2c351a12af72e --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml @@ -0,0 +1,67 @@ +<?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="StoreViewLanguageCorrectSwitchTest"> + <annotations> + <features value="Cms"/> + <title value="Check that Store View(language) switches correct"/> + <description value="Check that Store View(language) switches correct"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96388"/> + <useCaseId value="MAGETWO-57337"/> + <group value="Cms"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create Cms Pages --> + <createData entity="_newDefaultCmsPage" stepKey="createFirstCmsPage"/> + <createData entity="_newDefaultCmsPage" stepKey="createSecondCmsPage"/> + </before> + <after> + <deleteData createDataKey="createFirstCmsPage" stepKey="deleteFirstCmsPage"/> + <deleteData createDataKey="createSecondCmsPage" stepKey="deleteSecondCmsPage"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create StoreView --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + + <!-- Add StoreView To Cms Page--> + <actionGroup ref="AddStoreViewToCmsPage" stepKey="gotToCmsPage"> + <argument name="CMSPage" value="$$createSecondCmsPage$$"/> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + + <!-- Check that Cms Page is open --> + <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage"/> + <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle"/> + + <!-- Switch StoreView and check that Cms Page is open --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> + <argument name="storeView" value="NewStoreViewData"/> + </actionGroup> + <amOnPage url="{{StorefrontHomePage.url}}/$$createSecondCmsPage.identifier$$" stepKey="gotToSecondCmsPage"/> + <see userInput="$$createSecondCmsPage.title$$" stepKey="seePageTitle1"/> + + <!--Open first Cms page on custom store view--> + <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage1"/> + <see userInput="Whoops, our bad..." stepKey="seePageError"/> + + <!--Switch to default store view and check Cms page--> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefualtStoreView"/> + <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle2"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 309f08a54aab6..7bec1e3601461 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -417,6 +417,10 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e ->method('setCollectRecursively') ->with(false) ->willReturnSelf(); + $storageCollectionMock->expects($this->once()) + ->method('setOrder') + ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) + ->willReturnSelf(); $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php index 54e0e17ab7ad6..a624823d02c13 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php @@ -118,7 +118,8 @@ public function testPrepareMetadata() 'config' => [ 'editorConfig' => [ 'enabled' => false - ] + ], + 'componentType' => \Magento\Ui\Component\Container::NAME ] ] ] diff --git a/app/code/Magento/Cms/Ui/Component/DataProvider.php b/app/code/Magento/Cms/Ui/Component/DataProvider.php index 5fc9c5a896037..b02dd6ba98ed0 100644 --- a/app/code/Magento/Cms/Ui/Component/DataProvider.php +++ b/app/code/Magento/Cms/Ui/Component/DataProvider.php @@ -13,6 +13,9 @@ use Magento\Framework\AuthorizationInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\Reporting; +/** + * DataProvider for cms ui. + */ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider { /** @@ -67,6 +70,8 @@ public function __construct( } /** + * Get authorization info. + * * @deprecated 101.0.7 * @return AuthorizationInterface|mixed */ @@ -95,7 +100,8 @@ public function prepareMetadata() 'config' => [ 'editorConfig' => [ 'enabled' => false - ] + ], + 'componentType' => \Magento\Ui\Component\Container::NAME ] ] ] diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml index da89991869929..44bd7d3ba3dda 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml @@ -21,7 +21,7 @@ $_height = $block->getImagesHeight(); data-size="<?= $block->escapeHtmlAttr($file->getSize()) ?>" data-mime-type="<?= $block->escapeHtmlAttr($file->getMimeType()) ?>" > - <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;width:<?= $block->escapeHtmlAttr($_width) ?>px;"> + <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;"> <?php if ($block->getFileThumbUrl($file)):?> <img src="<?= $block->escapeHtmlAttr($block->getFileThumbUrl($file)) ?>" alt="<?= $block->escapeHtmlAttr($block->getFileName($file)) ?>"/> <?php endif; ?> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index 9f886f6f1345e..793fc7d26cb4a 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -146,7 +146,6 @@ <editor> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> - <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> </validation> <editorType>text</editorType> </editor> diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index c237d0ea9963a..c63ccae871657 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\App\Config\Type; use Magento\Framework\App\Config\ConfigSourceInterface; @@ -13,10 +14,12 @@ use Magento\Config\App\Config\Type\System\Reader; use Magento\Framework\App\ScopeInterface; use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; -use Magento\Store\Model\ScopeInterface as StoreScope; use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Model\ScopeInterface as StoreScope; /** * System configuration type @@ -24,12 +27,25 @@ * @api * @since 100.1.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ class System implements ConfigTypeInterface { + /** + * Config cache tag. + */ const CACHE_TAG = 'config_scopes'; + + /** + * System config type. + */ const CONFIG_TYPE = 'system'; + /** + * @var string + */ + private static $lockName = 'SYSTEM_CONFIG'; + /** * @var array */ @@ -76,6 +92,11 @@ class System implements ConfigTypeInterface */ private $encryptor; + /** + * @var LockGuardedCacheLoader + */ + private $lockQuery; + /** * @param ConfigSourceInterface $source * @param PostProcessorInterface $postProcessor @@ -87,7 +108,8 @@ class System implements ConfigTypeInterface * @param string $configType * @param Reader|null $reader * @param Encryptor|null $encryptor - * + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -101,14 +123,19 @@ public function __construct( $cachingNestedLevel = 1, $configType = self::CONFIG_TYPE, Reader $reader = null, - Encryptor $encryptor = null + Encryptor $encryptor = null, + LockManagerInterface $locker = null, + LockGuardedCacheLoader $lockQuery = null ) { $this->postProcessor = $postProcessor; $this->cache = $cache; $this->serializer = $serializer; $this->configType = $configType; $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); - $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(\Magento\Framework\Encryption\Encryptor::class); + $this->encryptor = $encryptor + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); } /** @@ -187,45 +214,56 @@ private function getWithParts($path) } /** - * Load configuration data for all scopes + * Load configuration data for all scopes. * * @return array */ private function loadAllData() { - $cachedData = $this->cache->load($this->configType); - - if ($cachedData === false) { - $data = $this->readData(); - } else { - $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); - } - - return $data; + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; + if ($cachedData !== false) { + $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); + } + return $data; + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Load configuration data for default scope + * Load configuration data for default scope. * * @param string $scopeType * @return array */ private function loadDefaultScopeData($scopeType) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType); - - if ($cachedData === false) { - $data = $this->readData(); - $this->cacheData($data); - } else { - $data = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; - } - - return $data; + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; + if ($cachedData !== false) { + $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + } + return $scopeData; + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Load configuration data for a specified scope + * Load configuration data for a specified scope. * * @param string $scopeType * @param string $scopeId @@ -233,31 +271,38 @@ private function loadDefaultScopeData($scopeType) */ private function loadScopeData($scopeType, $scopeId) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); - - if ($cachedData === false) { - if ($this->availableDataScopes === null) { - $cachedScopeData = $this->cache->load($this->configType . '_scopes'); - if ($cachedScopeData !== false) { - $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); - $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; + if ($cachedData === false) { + if ($this->availableDataScopes === null) { + $cachedScopeData = $this->cache->load($this->configType . '_scopes'); + if ($cachedScopeData !== false) { + $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); + $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); + } } + if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { + $scopeData = [$scopeType => [$scopeId => []]]; + } + } else { + $serializedCachedData = $this->encryptor->decrypt($cachedData); + $scopeData = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; } - if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { - return [$scopeType => [$scopeId => []]]; - } - $data = $this->readData(); - $this->cacheData($data); - } else { - $serializedCachedData = $this->encryptor->decrypt($cachedData); - $data = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; - } - return $data; + return $scopeData; + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Cache configuration data + * Cache configuration data. * * Caches data per scope to avoid reading data for all scopes on every request * @@ -295,7 +340,7 @@ private function cacheData(array $data) } /** - * Walk nested hash map by keys from $pathParts + * Walk nested hash map by keys from $pathParts. * * @param array $data to walk in * @param array $pathParts keys path @@ -332,7 +377,7 @@ private function readData(): array } /** - * Clean cache and global variables cache + * Clean cache and global variables cache. * * Next items cleared: * - Internal property intended to store already loaded configuration data @@ -344,6 +389,13 @@ private function readData(): array public function clean() { $this->data = []; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $cleanAction = function () { + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction + ); } } diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index 81e39a83296d7..8378c058c1955 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -134,6 +134,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory * @param \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory * @param array $data + * @param SettingChecker|null $settingChecker */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -143,13 +144,15 @@ public function __construct( \Magento\Config\Model\Config\Structure $configStructure, \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory, \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory, - array $data = [] + array $data = [], + SettingChecker $settingChecker = null ) { parent::__construct($context, $registry, $formFactory, $data); $this->_configFactory = $configFactory; $this->_configStructure = $configStructure; $this->_fieldsetFactory = $fieldsetFactory; $this->_fieldFactory = $fieldFactory; + $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); $this->_scopeLabels = [ self::SCOPE_DEFAULT => __('[GLOBAL]'), @@ -158,18 +161,6 @@ public function __construct( ]; } - /** - * @deprecated 100.1.2 - * @return SettingChecker - */ - private function getSettingChecker() - { - if ($this->settingChecker === null) { - $this->settingChecker = ObjectManager::getInstance()->get(SettingChecker::class); - } - return $this->settingChecker; - } - /** * Initialize objects required to render config form * @@ -366,9 +357,8 @@ protected function _initElement( $sharedClass = $this->_getSharedCssClass($field); $requiresClass = $this->_getRequiresCssClass($field, $fieldPrefix); + $isReadOnly = $this->isReadOnly($field, $path); - $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) - ?: $this->getSettingChecker()->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); $formField = $fieldset->addField( $elementId, $field->getType(), @@ -417,7 +407,7 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie { $data = $this->getAppConfigDataValue($path); - $placeholderValue = $this->getSettingChecker()->getPlaceholderValue( + $placeholderValue = $this->settingChecker->getPlaceholderValue( $path, $this->getScope(), $this->getStringScopeCode() @@ -434,6 +424,10 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie $backendModel = $field->getBackendModel(); // Backend models which implement ProcessorInterface are processed by ScopeConfigInterface if (!$backendModel instanceof ProcessorInterface) { + if (array_key_exists($path, $this->_configData)) { + $data = $this->_configData[$path]; + } + $backendModel->setPath($path) ->setValue($data) ->setWebsite($this->getWebsiteCode()) @@ -541,7 +535,7 @@ public function getConfigValue($path) } /** - * @return \Magento\Backend\Block\Widget\Form|\Magento\Framework\View\Element\AbstractBlock + * @inheritdoc */ protected function _beforeToHtml() { @@ -718,6 +712,7 @@ protected function _getAdditionalElementTypes() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSectionCode() { @@ -729,6 +724,7 @@ public function getSectionCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getWebsiteCode() { @@ -740,6 +736,7 @@ public function getWebsiteCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreCode() { @@ -797,6 +794,26 @@ private function getAppConfig() return $this->appConfig; } + /** + * Check Path is Readonly + * + * @param \Magento\Config\Model\Config\Structure\Element\Field $field + * @param string $path + * @return boolean + */ + private function isReadOnly(\Magento\Config\Model\Config\Structure\Element\Field $field, $path) + { + $isReadOnly = $this->settingChecker->isReadOnly( + $path, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + if (!$isReadOnly) { + $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) + ?: $this->settingChecker->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); + } + return $isReadOnly; + } + /** * Retrieve deployment config data value by path * diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php index 86ae1f96749df..c622a48b7f2c8 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php @@ -90,10 +90,10 @@ public function process($path, $value, $scope, $scopeCode) } try { - $config = $this->configFactory->create([ + $config = $this->configFactory->create(['data' => [ 'scope' => $scope, 'scope_code' => $scopeCode, - ]); + ]]); $config->setDataByPath($path, $value); $config->save(); } catch (\Exception $exception) { diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index b1074e92cc949..bd38d1451e1b6 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -114,6 +114,11 @@ class Config extends \Magento\Framework\DataObject */ private $scopeTypeNormalizer; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -126,6 +131,7 @@ class Config extends \Magento\Framework\DataObject * @param array $data * @param ScopeResolverPool|null $scopeResolverPool * @param ScopeTypeNormalizer|null $scopeTypeNormalizer + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -139,7 +145,8 @@ public function __construct( SettingChecker $settingChecker = null, array $data = [], ScopeResolverPool $scopeResolverPool = null, - ScopeTypeNormalizer $scopeTypeNormalizer = null + ScopeTypeNormalizer $scopeTypeNormalizer = null, + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { parent::__construct($data); $this->_eventManager = $eventManager; @@ -155,6 +162,8 @@ public function __construct( ?? ObjectManager::getInstance()->get(ScopeResolverPool::class); $this->scopeTypeNormalizer = $scopeTypeNormalizer ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); } /** @@ -224,6 +233,8 @@ public function save() throw $e; } + $this->pillPut->put(); + return $this; } @@ -424,6 +435,11 @@ protected function _processGroup( if (!isset($fieldData['value'])) { $fieldData['value'] = null; } + + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { + $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); + } + $data = [ 'field' => $fieldId, 'groups' => $groups, @@ -520,24 +536,29 @@ public function setDataByPath($path, $value) if ($path === '') { throw new \UnexpectedValueException('Path must not be empty'); } + $pathParts = explode('/', $path); $keyDepth = count($pathParts); - if ($keyDepth !== 3) { + if ($keyDepth < 3) { throw new \UnexpectedValueException( - "Allowed depth of configuration is 3 (<section>/<group>/<field>). Your configuration depth is " - . $keyDepth . " for path '$path'" + 'Minimal depth of configuration is 3. Your configuration depth is ' . $keyDepth ); } + + $section = array_shift($pathParts); $data = [ - 'section' => $pathParts[0], - 'groups' => [ - $pathParts[1] => [ - 'fields' => [ - $pathParts[2] => ['value' => $value], - ], - ], + 'fields' => [ + array_pop($pathParts) => ['value' => $value], ], ]; + while ($pathParts) { + $data = [ + 'groups' => [ + array_pop($pathParts) => $data, + ], + ]; + } + $data['section'] = $section; $this->addData($data); } diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php index 9a483de6a695b..f5d568f2f36be 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php @@ -10,6 +10,8 @@ namespace Magento\Config\Model\Config\Backend\Admin; /** + * Process custom admin url during configuration value save process. + * * @api * @since 100.0.2 */ @@ -56,8 +58,9 @@ public function beforeSave() { $value = $this->getValue(); if ($value == 1) { - $customUrl = $this->getData('groups/url/fields/custom/value'); - if (empty($customUrl)) { + $customUrlField = $this->getData('groups/url/fields/custom/value'); + $customUrlConfig = $this->_config->getValue('admin/url/custom'); + if (empty($customUrlField) && empty($customUrlConfig)) { throw new \Magento\Framework\Exception\LocalizedException(__('Please specify the admin custom URL.')); } } diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php index 3d5713357c39c..6e0b6275db836 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php +++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php @@ -9,6 +9,8 @@ use Magento\Framework\Serialize\Serializer\Json; /** + * Serialized backend model + * * @api * @since 100.0.2 */ @@ -46,17 +48,32 @@ public function __construct( } /** + * Processing object after load data + * * @return void */ protected function _afterLoad() { $value = $this->getValue(); if (!is_array($value)) { - $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + try { + $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + } catch (\Exception $e) { + $this->_logger->critical( + sprintf( + 'Failed to unserialize %s config value. The error is: %s', + $this->getPath(), + $e->getMessage() + ) + ); + $this->setValue(false); + } } } /** + * Processing object before save data + * * @return $this */ public function beforeSave() diff --git a/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php b/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php index bf414890d0ed4..830b6376c94bc 100644 --- a/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php +++ b/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php @@ -3,10 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Config\Observer\Config\Backend\Admin; use Magento\Framework\Event\ObserverInterface; +/** + * Class AfterCustomUrlChangedObserver redirects to new custom admin URL. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class AfterCustomUrlChangedObserver implements ObserverInterface { /** @@ -56,7 +63,6 @@ public function __construct( * * @param \Magento\Framework\Event\Observer $observer * @return void - * @SuppressWarnings(PHPMD.ExitExpression) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute(\Magento\Framework\Event\Observer $observer) @@ -68,6 +74,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->_authSession->destroy(); $adminUrl = $this->_backendData->getHomePageUrl(); $this->_response->setRedirect($adminUrl)->sendResponse(); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } } diff --git a/app/code/Magento/Config/Setup/ConfigOptionsList.php b/app/code/Magento/Config/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..c410eeae615e5 --- /dev/null +++ b/app/code/Magento/Config/Setup/ConfigOptionsList.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\Data\ConfigDataFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\SelectConfigOption; + +/** + * Deployment configuration options required for the Config module. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + /** + * Input key for the debug_logging option. + */ + const INPUT_KEY_DEBUG_LOGGING = 'enable-debug-logging'; + + /** + * Path to the debug_logging value in the deployment config. + */ + const CONFIG_PATH_DEBUG_LOGGING = 'dev/debug/debug_logging'; + + /** + * Input key for the syslog_logging option. + */ + const INPUT_KEY_SYSLOG_LOGGING = 'enable-syslog-logging'; + + /** + * Path to the syslog_logging value in the deployment config. + */ + const CONFIG_PATH_SYSLOG_LOGGING = 'dev/syslog/syslog_logging'; + + /** + * @var ConfigDataFactory + */ + private $configDataFactory; + + /** + * @param ConfigDataFactory $configDataFactory + */ + public function __construct(ConfigDataFactory $configDataFactory) + { + $this->configDataFactory = $configDataFactory; + } + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_DEBUG_LOGGING, + SelectConfigOption::FRONTEND_WIZARD_RADIO, + [true, false, 1, 0], + self::CONFIG_PATH_DEBUG_LOGGING, + 'Enable debug logging' + ), + new SelectConfigOption( + self::INPUT_KEY_SYSLOG_LOGGING, + SelectConfigOption::FRONTEND_WIZARD_RADIO, + [true, false, 1, 0], + self::CONFIG_PATH_SYSLOG_LOGGING, + 'Enable syslog logging' + ), + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $deploymentOption = [ + self::INPUT_KEY_DEBUG_LOGGING => self::CONFIG_PATH_DEBUG_LOGGING, + self::INPUT_KEY_SYSLOG_LOGGING => self::CONFIG_PATH_SYSLOG_LOGGING, + ]; + + $config = []; + foreach ($deploymentOption as $inputKey => $configPath) { + $configValue = $this->processBooleanConfigValue( + $inputKey, + $configPath, + $options + ); + if ($configValue) { + $config[] = $configValue; + } + } + + return $config; + } + + /** + * Provide config value from input. + * + * @param string $inputKey + * @param string $configPath + * @param array $options + * @return ConfigData|null + */ + private function processBooleanConfigValue(string $inputKey, string $configPath, array &$options): ?ConfigData + { + $configData = null; + if (isset($options[$inputKey])) { + $configData = $this->configDataFactory->create(ConfigFilePool::APP_ENV); + if ($options[$inputKey] === 'true' + || $options[$inputKey] === '1') { + $value = 1; + } else { + $value = 0; + } + $configData->set($configPath, $value); + } + + return $configData; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + return []; + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml index 06c041fabeb35..4e9319351a130 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml @@ -28,4 +28,32 @@ <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> </actionGroup> - </actionGroups> \ No newline at end of file + <actionGroup name="SetTaxApplyOnSetting"> + <arguments> + <argument name="userInput" type="string"/> + </arguments> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> + <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" x="0" y="-80" stepKey="goToCheckbox"/> + <uncheckOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="enableApplyTaxOnSetting"/> + <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOn"/> + <scrollTo selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="scrollToTop"/> + <click selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" stepKey="collapseCalcSettingsTab"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="DisableTaxApplyOnOriginalPrice"> + <arguments> + <argument name="userInput" type="string"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> + <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" x="0" y="-80" stepKey="goToCheckbox"/> + <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOff"/> + <checkOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="disableApplyTaxOnSetting"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml index 771f0035b82b9..eefaf5f3b539c 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml @@ -12,15 +12,15 @@ <magentoCLI stepKey="enableWYSIWYG" command="config:set cms/wysiwyg/enabled enabled"/> </actionGroup> <actionGroup name="SwitchToTinyMCE3"> - <comment userInput="Choose TinyMCE3 as the default editor" stepKey="chooseTinyMCE3AsEditor"/> - <conditionalClick stepKey="expandWYSIWYGOptions1" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> - <waitForElementVisible selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="waitForCheckbox2" /> - <uncheckOption selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="uncheckUseSystemValue2"/> - <waitForElementVisible selector="{{ContentManagementSection.Switcher}}" stepKey="waitForSwitcherDropdown2" /> - <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 3" stepKey="switchToVersion3" /> - <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> - <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> - <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> + <comment userInput="Choose TinyMCE3 as the default editor" stepKey="chooseTinyMCE3AsEditor"/> + <conditionalClick stepKey="expandWYSIWYGOptions1" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> + <waitForElementVisible selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="waitForCheckbox2" /> + <uncheckOption selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="uncheckUseSystemValue2"/> + <waitForElementVisible selector="{{ContentManagementSection.Switcher}}" stepKey="waitForSwitcherDropdown2" /> + <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 3" stepKey="switchToVersion3" /> + <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> </actionGroup> <actionGroup name="DisabledWYSIWYG"> <magentoCLI stepKey="disableWYSIWYG" command="config:set cms/wysiwyg/enabled disabled"/> @@ -38,4 +38,15 @@ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> <waitForPageLoad stepKey="waitForPageLoad2" /> </actionGroup> + <actionGroup name="EnabledWYSIWYGEditor"> + <amOnPage url="{{AdminContentManagementPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.EnableWYSIWYG}}" visible="false" stepKey="expandWYSIWYGOptionsTab"/> + <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitTabToExpand"/> + <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="enableEnableSystemValue"/> + <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="enableWYSIWYG"/> + <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptionsTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> + <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml b/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml new file mode 100644 index 0000000000000..f89cdf1a87b31 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml @@ -0,0 +1,24 @@ +<?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="EnableAllowGuestCheckout" type="allow_guest_checkout_config"> + <requiredEntity type="guest_checkout">AllowGuestCheckoutYes</requiredEntity> + </entity> + <entity name="AllowGuestCheckoutYes" type="guest_checkout"> + <data key="value">1</data> + </entity> + + <entity name="DisableAllowGuestCheckout" type="allow_guest_checkout_config"> + <requiredEntity type="guest_checkout">AllowGuestCheckoutNo</requiredEntity> + </entity> + <entity name="AllowGuestCheckoutNo" type="guest_checkout"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml new file mode 100644 index 0000000000000..53ca46e746206 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml @@ -0,0 +1,24 @@ +<?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="EnableAdminAccountAllowCountry" type="admin_account_country_options_config"> + <requiredEntity type="admin_account_country_options_value">AdminAccountAllowCountryUS</requiredEntity> + </entity> + <entity name="AdminAccountAllowCountryUS" type="admin_account_country_options_value"> + <data key="value">US</data> + </entity> + + <entity name="DisableAdminAccountAllowCountry" type="default_admin_account_country_options_config"> + <requiredEntity type="checkoutTotalFlagZero">DefaultAdminAccountAllowCountry</requiredEntity> + </entity> + <entity name="DefaultAdminAccountAllowCountry" type="checkoutTotalFlagZero"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml new file mode 100644 index 0000000000000..5647283fae181 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml @@ -0,0 +1,24 @@ +<?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="SetLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">setLocaleOptionsFrance</requiredEntity> + </entity> + <entity name="setLocaleOptionsFrance" type="code"> + <data key="value">fr_FR</data> + </entity> + + <entity name="DefaultLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">setLocaleOptionsUSA</requiredEntity> + </entity> + <entity name="setLocaleOptionsUSA" type="code"> + <data key="value">en_US</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml index 75dc19dc99c8e..85188eb6e04cb 100644 --- a/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml +++ b/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="AdminAccountSharingYes" type="admin_account_sharing_value"> <data key="value">Yes</data> </entity> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml new file mode 100644 index 0000000000000..052d9b6574774 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml @@ -0,0 +1,21 @@ +<?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="AllowGuestCheckoutConfig" dataType="allow_guest_checkout_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/checkout/" method="POST"> + <object key="groups" dataType="allow_guest_checkout_config"> + <object key="options" dataType="allow_guest_checkout_config"> + <object key="fields" dataType="allow_guest_checkout_config"> + <object key="guest_checkout" dataType="guest_checkout"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml new file mode 100644 index 0000000000000..055a9896cd2d2 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml @@ -0,0 +1,21 @@ +<?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="GeneralLocaleOptionsConfig" dataType="locale_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> + <object key="groups" dataType="locale_options_config"> + <object key="locale" dataType="locale_options_config"> + <object key="fields" dataType="locale_options_config"> + <object key="code" dataType="code"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml new file mode 100644 index 0000000000000..bd16c225af51d --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml @@ -0,0 +1,35 @@ +<?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="AdminAccountCountryOptionConfig" dataType="admin_account_country_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> + <object key="groups" dataType="admin_account_country_options_config"> + <object key="country" dataType="admin_account_country_options_config"> + <object key="fields" dataType="admin_account_country_options_config"> + <object key="allow" dataType="admin_account_country_options_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> + + <operation name="DefaultAdminAccountCountryOptionConfig" dataType="default_admin_account_country_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> + <object key="groups" dataType="default_admin_account_country_options_config"> + <object key="country" dataType="default_admin_account_country_options_config"> + <object key="fields" dataType="default_admin_account_country_options_config"> + <object key="allow" dataType="default_admin_account_country_options_config"> + <object key="inherit" dataType="checkoutTotalFlagZero"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml index 37b8414d1f396..e7544c4e8ae28 100644 --- a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml +++ b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="AdminAccountSharingConfig" dataType="admin_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/admin/" method="POST"> <object key="groups" dataType="admin_account_sharing_config"> <object key="security" dataType="admin_account_sharing_config"> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml index 8a56c2777084e..b5bfe9cc2ea05 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml @@ -11,5 +11,7 @@ <element name="generalTab" type="text" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='General']"/> <element name="generalTabClosed" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='false' or @aria-expanded='0']//strong[text()='General']"/> <element name="generalTabOpened" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='true' or @aria-expanded='1']//strong[text()='General']"/> + <element name="defaultConfigButton" type="button" selector="#store-change-button" timeout="30"/> + <element name="defaultConfigDropdown" type="button" selector="//ul[@class='dropdown-menu']" timeout="30"/> </section> -</sections> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml index c48f33ba06b3b..e999dbc42a6af 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml @@ -17,5 +17,9 @@ <element name="catalogPriceScopeValue" type="select" selector="//select[@id='catalog_price_scope']/option[text()='{{args}}']" parameterized="true"/> <element name="defaultProductPrice" type="input" selector="#catalog_price_default_product_price"/> <element name="save" type="button" selector="#save"/> + <element name="flatCatalogCategoryCheckBox" type="checkbox" selector="#catalog_frontend_flat_catalog_category_inherit"/> + <element name="flatCatalogCategory" type="select" selector="#catalog_frontend_flat_catalog_category"/> + <element name="flatCatalogProduct" type="select" selector="#catalog_frontend_flat_catalog_product"/> + <element name="successMessage" type="text" selector="#messages"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml index 66b40f74d8e98..d007c860782aa 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml @@ -17,6 +17,7 @@ <element name="Switcher" type="button" selector="#cms_wysiwyg_editor" /> <element name="StaticURL" type="button" selector="#cms_wysiwyg_use_static_urls_in_catalog" /> <element name="Save" type="button" selector="#save" timeout="30"/> + <element name="StoreConfigurationPageSuccessMessage" type="text" selector="#messages [data-ui-id='messages-message-success']"/> </section> <section name="WebSection"> <element name="DefaultLayoutsTab" type="button" selector="#web_default_layouts-head"/> diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml new file mode 100644 index 0000000000000..b0a7ee07ddad0 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.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="CheckingCountryDropDownWithOneAllowedCountryTest"> + <annotations> + <features value="Config"/> + <stories value="MAGETWO-96107: Additional blank option in country dropdown"/> + <title value="Checking country drop-down with one allowed country"/> + <description value="Check country drop-down with one allowed country"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96133"/> + <group value="configuration"/> + </annotations> + <before> + <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> + </before> + <after> + <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="CustomerEntityOne.email"/> + </actionGroup> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearFilters"/> + <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Flush Magento Cache--> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <!--Create a customer account from Storefront--> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="createAnAccount"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + <click selector="{{CheckoutPaymentSection.addressBook}}" stepKey="goToAddressBook"/> + <click selector="{{StorefrontCustomerAddressSection.country}}" stepKey="clickToExpandCountryDropDown"/> + <see selector="{{StorefrontCustomerAddressSection.country}}" userInput="United States" stepKey="seeSelectedCountry"/> + <dontSee selector="{{StorefrontCustomerAddressSection.country}}" userInput="Brazil" stepKey="canNotSeeSelectedCountry"/> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php index de18d45d26864..31215f1bdee2b 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php @@ -40,7 +40,11 @@ protected function setUp() $this->file = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\File::class, - ['data' => $this->testData] + [ + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class), + 'data' => $this->testData, + + ] ); $formMock = new \Magento\Framework\DataObject(); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index 8a005a52ab614..b752f79f73446 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -34,6 +34,7 @@ protected function setUp() \Magento\Config\Block\System\Config\Form\Field\Image::class, [ 'urlBuilder' => $this->urlBuilderMock, + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) ] ); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php index f5c65e848b3bf..e7ba2e8aaa2e7 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php @@ -21,7 +21,10 @@ protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( - \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class + \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class, + [ + '_escaper' => $testHelper->getObject(\Magento\Framework\Escaper::class) + ] ); $this->_object->setData('html_id', 'spec_element'); $this->_formMock = $this->createPartialMock( diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php index 93650dd62657c..4e260b0fb2bb1 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php @@ -102,6 +102,9 @@ protected function setUp() \Magento\Config\Block\System\Config\Form\Fieldset\Factory::class ); $this->_fieldFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Field\Factory::class); + $settingCheckerMock = $this->getMockBuilder(SettingChecker::class) + ->disableOriginalConstructor() + ->getMock(); $this->_coreConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->_backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); @@ -153,6 +156,7 @@ protected function setUp() 'fieldsetFactory' => $this->_fieldsetFactoryMock, 'fieldFactory' => $this->_fieldFactoryMock, 'context' => $context, + 'settingChecker' => $settingCheckerMock, ]; $objectArguments = $helper->getConstructArguments(\Magento\Config\Block\System\Config\Form::class, $data); @@ -532,7 +536,7 @@ public function testInitFields( $elementVisibilityMock = $this->getMockBuilder(ElementVisibilityInterface::class) ->getMockForAbstractClass(); - $elementVisibilityMock->expects($this->once()) + $elementVisibilityMock->expects($this->any()) ->method('isDisabled') ->willReturn($isDisabled); diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php index edb76c067bf35..35b2406b328cb 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php @@ -103,7 +103,7 @@ public function testProcess($path, $value, $scope, $scopeCode) $config = $this->createMock(Config::class); $this->configFactory->expects($this->once()) ->method('create') - ->with(['scope' => $scope, 'scope_code' => $scopeCode]) + ->with(['data' => ['scope' => $scope, 'scope_code' => $scopeCode]]) ->willReturn($config); $config->expects($this->once()) ->method('setDataByPath') diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php index bb1e0e0225901..c2685e0a265cd 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php @@ -9,7 +9,11 @@ use Magento\Framework\Model\Context; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Psr\Log\LoggerInterface; +/** + * Class SerializedTest + */ class SerializedTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Config\Model\Config\Backend\Serialized */ @@ -18,14 +22,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase /** @var Json|\PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $loggerMock; + protected function setUp() { $objectManager = new ObjectManager($this); $this->serializerMock = $this->createMock(Json::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); $contextMock = $this->createMock(Context::class); $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $contextMock->method('getEventDispatcher') ->willReturn($eventManagerMock); + $contextMock->method('getLogger') + ->willReturn($this->loggerMock); $this->serializedConfig = $objectManager->getObject( Serialized::class, [ @@ -72,6 +82,20 @@ public function afterLoadDataProvider() ]; } + public function testAfterLoadWithException() + { + $value = '{"key":'; + $expected = false; + $this->serializedConfig->setValue($value); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('critical'); + $this->serializedConfig->afterLoad(); + $this->assertEquals($expected, $this->serializedConfig->getValue()); + } + /** * @param string $expected * @param int|double|string|array|boolean|null $value diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index bdcb44b756bb2..66163e354cc06 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -330,22 +330,59 @@ public function testSaveToCheckScopeDataSet() $this->model->save(); } - public function testSetDataByPath() + /** + * @param string $path + * @param string $value + * @param string $section + * @param array $groups + * @dataProvider setDataByPathDataProvider + */ + public function testSetDataByPath(string $path, string $value, string $section, array $groups) { - $value = 'value'; - $path = '<section>/<group>/<field>'; $this->model->setDataByPath($path, $value); - $expected = [ - 'section' => '<section>', - 'groups' => [ - '<group>' => [ - 'fields' => [ - '<field>' => ['value' => $value], + $this->assertEquals($section, $this->model->getData('section')); + $this->assertEquals($groups, $this->model->getData('groups')); + } + + /** + * @return array + */ + public function setDataByPathDataProvider(): array + { + return [ + 'depth 3' => [ + 'a/b/c', + 'value1', + 'a', + [ + 'b' => [ + 'fields' => [ + 'c' => ['value' => 'value1'], + ], + ], + ], + ], + 'depth 5' => [ + 'a/b/c/d/e', + 'value1', + 'a', + [ + 'b' => [ + 'groups' => [ + 'c' => [ + 'groups' => [ + 'd' => [ + 'fields' => [ + 'e' => ['value' => 'value1'], + ], + ], + ], + ], + ], ], ], ], ]; - $this->assertSame($expected, $this->model->getData()); } /** @@ -359,14 +396,13 @@ public function testSetDataByPathEmpty() /** * @param string $path - * @param string $expectedException - * * @dataProvider setDataByPathWrongDepthDataProvider */ - public function testSetDataByPathWrongDepth($path, $expectedException) + public function testSetDataByPathWrongDepth(string $path) { - $expectedException = 'Allowed depth of configuration is 3 (<section>/<group>/<field>). ' . $expectedException; - $this->expectException('\UnexpectedValueException'); + $currentDepth = count(explode('/', $path)); + $expectedException = 'Minimal depth of configuration is 3. Your configuration depth is ' . $currentDepth; + $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage($expectedException); $value = 'value'; $this->model->setDataByPath($path, $value); @@ -375,13 +411,11 @@ public function testSetDataByPathWrongDepth($path, $expectedException) /** * @return array */ - public function setDataByPathWrongDepthDataProvider() + public function setDataByPathWrongDepthDataProvider(): array { return [ - 'depth 2' => ['section/group', "Your configuration depth is 2 for path 'section/group'"], - 'depth 1' => ['section', "Your configuration depth is 1 for path 'section'"], - 'depth 4' => ['section/group/field/sub-field', "Your configuration depth is 4 for path" - . " 'section/group/field/sub-field'", ], + 'depth 2' => ['section/group'], + 'depth 1' => ['section'], ]; } } diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 57c067d2cae27..3312fb630ccda 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-message-queue": "*", "magento/module-backend": "*", "magento/module-cron": "*", "magento/module-deploy": "*", diff --git a/app/code/Magento/Config/etc/adminhtml/di.xml b/app/code/Magento/Config/etc/adminhtml/di.xml index 5e54f177776ba..189fbdf69a7e8 100644 --- a/app/code/Magento/Config/etc/adminhtml/di.xml +++ b/app/code/Magento/Config/etc/adminhtml/di.xml @@ -6,7 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\Config\Model\Config\Structure\SearchInterface" type="Magento\Config\Model\Config\Structure" /> <preference for="Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface" type="Magento\Config\Model\Config\Backend\File\RequestData" /> <preference for="Magento\Config\Model\Config\Structure\ElementVisibilityInterface" type="Magento\Config\Model\Config\Structure\ElementVisibilityComposite" /> <type name="Magento\Config\Model\Config\Structure\Element\Iterator\Tab" shared="false" /> diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index a5dd18097fb47..920cac382fcbf 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -77,6 +77,11 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Lock\Backend\Cache"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> + </arguments> + </type> <type name="Magento\Config\App\Config\Type\System"> <arguments> <argument name="source" xsi:type="object">systemConfigSourceAggregatedProxy</argument> @@ -85,8 +90,18 @@ <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> + <argument name="lockQuery" xsi:type="object">systemConfigQueryLocker</argument> </arguments> </type> + + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockTimeout" xsi:type="number">42000</argument> + <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + <type name="Magento\Config\App\Config\Type\System\Reader"> <arguments> <argument name="source" xsi:type="object">systemConfigSourceAggregated</argument> diff --git a/app/code/Magento/ConfigurableImportExport/etc/module.xml b/app/code/Magento/ConfigurableImportExport/etc/module.xml index 7ff81f8d63443..b59234ca0e7da 100644 --- a/app/code/Magento/ConfigurableImportExport/etc/module.xml +++ b/app/code/Magento/ConfigurableImportExport/etc/module.xml @@ -6,6 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_ConfigurableImportExport" > + <module name="Magento_ConfigurableImportExport"> + <sequence> + <module name="Magento_ConfigurableProduct"/> + </sequence> </module> </config> diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index 2502b79921e99..e07879e93a6b4 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -15,6 +15,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Confugurable product view type + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api @@ -276,6 +278,8 @@ protected function getOptionImages() } /** + * Collect price options + * * @return array */ protected function getOptionPrices() @@ -314,6 +318,11 @@ protected function getOptionPrices() ), ], 'tierPrices' => $tierPrices, + 'msrpPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $product->getMsrp() + ), + ], ]; } return $prices; diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 79c2dd812acf1..2f07f8b90ce7e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,6 +10,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +/** + * Configurable product link management. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementInterface { /** @@ -68,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getChildren($sku) { @@ -107,11 +111,15 @@ public function getChildren($sku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function addChild($sku, $childSku) { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $child = $this->productRepository->get($childSku); $childrenIds = array_values($this->configurableType->getChildrenIds($product->getId())[0]); @@ -150,7 +158,11 @@ public function addChild($sku, $childSku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function removeChild($sku, $childSku) { diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php new file mode 100644 index 0000000000000..92b7ab0d88ea8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Extender of product identities for child of configurable products + */ +class ProductIdentitiesExtender +{ + /** + * @var Configurable + */ + private $configurableType; + + /** + * @param Configurable $configurableType + */ + public function __construct(Configurable $configurableType) + { + $this->configurableType = $configurableType; + } + + /** + * Add child identities to product identities + * + * @param Product $subject + * @param array $identities + * @return array + */ + public function afterGetIdentities(Product $subject, array $identities): array + { + foreach ($this->configurableType->getChildrenIds($subject->getId()) as $childIds) { + foreach ($childIds as $childId) { + $identities[] = Product::CACHE_TAG . '_' . $childId; + } + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index f98075f2294cc..a849d964eaed5 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -24,6 +24,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -1231,6 +1232,8 @@ public function isPossibleBuyFromList($product) /** * Returns array of sub-products for specified configurable product * + * $requiredAttributeIds - one dimensional array, if provided + * * Result array contains all children for specified configurable product * * @param \Magento\Catalog\Model\Product $product @@ -1244,9 +1247,12 @@ public function getUsedProducts($product, $requiredAttributeIds = null) __METHOD__, $product->getData($metadata->getLinkField()), $product->getStoreId(), - $this->getCustomerSession()->getCustomerGroupId(), - $requiredAttributeIds + $this->getCustomerSession()->getCustomerGroupId() ]; + if ($requiredAttributeIds !== null) { + sort($requiredAttributeIds); + $keyParts[] = implode('', $requiredAttributeIds); + } $cacheKey = $this->getUsedProductsCacheKey($keyParts); return $this->loadUsedProducts($product, $cacheKey); } @@ -1385,7 +1391,7 @@ function ($item) { */ private function getUsedProductsCacheKey($keyParts) { - return md5(implode('_', $keyParts)); + return sha1(implode('_', $keyParts)); } /** diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php index bee334596e990..f2bf3116af9e4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,14 +7,15 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class Price for configurable product + */ class Price extends \Magento\Catalog\Model\Product\Type\Price { /** - * Get product final price - * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @inheritdoc */ public function getFinalPrice($qty, $product) { @@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { - $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); + /** @var Product $simpleProduct */ + $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); + $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); + $finalPrice = parent::getFinalPrice($qty, $simpleProduct); } else { $priceInfo = $product->getPriceInfo(); $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); @@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrice($product) { @@ -48,6 +52,7 @@ public function getPrice($product) } } } + return 0; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index 73e7f9053fa4a..1bd8ef59f0d6d 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -203,7 +203,9 @@ protected function fillSimpleProductData( $postData['stock_data'] = array_diff_key((array)$parentProduct->getStockData(), array_flip($keysFilter)); if (!isset($postData['stock_data']['is_in_stock'])) { $stockStatus = $parentProduct->getQuantityAndStockStatus(); - $postData['stock_data']['is_in_stock'] = $stockStatus['is_in_stock']; + if (isset($stockStatus['is_in_stock'])) { + $postData['stock_data']['is_in_stock'] = $stockStatus['is_in_stock']; + } } $postData = $this->processMediaGallery($product, $postData); $postData['status'] = isset($postData['status']) @@ -262,6 +264,8 @@ public function duplicateImagesForVariations($productsData) } /** + * Process media gallery for product + * * @param \Magento\Catalog\Model\Product $product * @param array $productData * diff --git a/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php new file mode 100644 index 0000000000000..1ed4432347b7a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; + +/** + * Class Product + * + * @package Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition + */ +class Product +{ + /** + * Prepare configurable product for validation. + * + * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject + * @param \Magento\Framework\Model\AbstractModel $model + * @return array + */ + public function beforeValidate( + \Magento\SalesRule\Model\Rule\Condition\Product $subject, + \Magento\Framework\Model\AbstractModel $model + ) { + $product = $this->getProductToValidate($subject, $model); + if ($model->getProduct() !== $product) { + // We need to replace product only for validation and keep original product for all other cases. + $clone = clone $model; + $clone->setProduct($product); + $model = $clone; + } + + return [$model]; + } + + /** + * Select proper product for validation. + * + * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject + * @param \Magento\Framework\Model\AbstractModel $model + * + * @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product + */ + private function getProductToValidate( + \Magento\SalesRule\Model\Rule\Condition\Product $subject, + \Magento\Framework\Model\AbstractModel $model + ) { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $model->getProduct(); + + $attrCode = $subject->getAttribute(); + + /* Check for attributes which are not available for configurable products */ + if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) { + /** @var \Magento\Catalog\Model\AbstractModel $childProduct */ + $childProduct = current($model->getChildren())->getProduct(); + if ($childProduct->hasData($attrCode)) { + $product = $childProduct; + } + } + + return $product; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php new file mode 100644 index 0000000000000..8bdde2aeb0cff --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; + +/** + * Plugin for CommonTaxCollector to apply Tax Class ID from child item for configurable product + */ +class CommonTaxCollector +{ + /** + * Apply Tax Class ID from child item for configurable product + * + * @param \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject + * @param QuoteDetailsItemInterface $result + * @param QuoteDetailsItemInterfaceFactory $itemDataObjectFactory + * @param AbstractItem $item + * @return QuoteDetailsItemInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMapItem( + \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject, + QuoteDetailsItemInterface $result, + QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, + AbstractItem $item + ) : QuoteDetailsItemInterface { + if ($item->getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) { + $childItem = $item->getChildren()[0]; + $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId()); + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php index 611523a60b06d..447ba16d72710 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Pricing\Render; +use Magento\Catalog\Pricing\Price\TierPrice; + /** * Responsible for displaying tier price box on configurable product page. * @@ -17,9 +19,27 @@ class TierPriceBox extends FinalPriceBox */ public function toHtml() { - // Hide tier price block in case of MSRP. - if (!$this->isMsrpPriceApplicable()) { + // Hide tier price block in case of MSRP or in case when no options with tier price. + if (!$this->isMsrpPriceApplicable() && $this->isTierPriceApplicable()) { return parent::toHtml(); } } + + /** + * Check if at least one of simple products has tier price. + * + * @return bool + */ + private function isTierPriceApplicable() + { + $product = $this->getSaleableItem(); + foreach ($product->getTypeInstance()->getUsedProducts($product) as $simpleProduct) { + if ($simpleProduct->isSalable() && + !empty($simpleProduct->getPriceInfo()->getPrice(TierPrice::PRICE_CODE)->getTierPriceList()) + ) { + return true; + } + } + return false; + } } diff --git a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php index f69d8529fb801..c6b173453f5ec 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php +++ b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php @@ -6,16 +6,16 @@ namespace Magento\ConfigurableProduct\Setup\Patch\Data; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Eav\Setup\EavSetup; use Magento\Eav\Setup\EavSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** * Class InstallInitialConfigurableAttributes + * * @package Magento\ConfigurableProduct\Setup\Patch */ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchVersionInterface @@ -24,6 +24,7 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV * @var ModuleDataSetupInterface */ private $moduleDataSetup; + /** * @var EavSetupFactory */ @@ -43,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -64,24 +65,27 @@ public function apply() 'color' ]; foreach ($attributes as $attributeCode) { - $relatedProductTypes = explode( - ',', - $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to') - ); - if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { - $relatedProductTypes[] = Configurable::TYPE_CODE; - $eavSetup->updateAttribute( - \Magento\Catalog\Model\Product::ENTITY, - $attributeCode, - 'apply_to', - implode(',', $relatedProductTypes) + $attribute = $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to'); + if ($attribute) { + $relatedProductTypes = explode( + ',', + $attribute ); + if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + $relatedProductTypes[] = Configurable::TYPE_CODE; + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + $attributeCode, + 'apply_to', + implode(',', $relatedProductTypes) + ); + } } } } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -89,7 +93,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -97,7 +101,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml new file mode 100644 index 0000000000000..4328159d6e930 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="addOptionsToAttributeActionGroup"> + <arguments> + <argument name="option1" defaultValue="colorProductAttribute2"/> + <argument name="option2" defaultValue="colorDefaultProductAttribute1"/> + <argument name="option3" defaultValue="colorProductAttribute3"/> + <argument name="option4" defaultValue="colorProductAttribute1"/> + <argument name="option5" defaultValue="colorDefaultProductAttribute2"/> + </arguments> + <!--Add option 1 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="{{option1.name}}" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <!--Add option 2 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption2" after="fillAdminLabel1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('2')}}" time="30" stepKey="waitForOptionRow2" after="clickAddOption2"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('1')}}" userInput="{{option2.name}}" stepKey="fillAdminLabel2" after="waitForOptionRow2"/> + <!--Add option 3 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption3" after="fillAdminLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('3')}}" time="30" stepKey="waitForOptionRow3" after="clickAddOption3"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('2')}}" userInput="{{option3.name}}" stepKey="fillAdminLabel3" after="waitForOptionRow3"/> + <!--Add option 4 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption4" after="fillAdminLabel3"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('4')}}" time="30" stepKey="waitForOptionRow4" after="clickAddOption4"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('3')}}" userInput="{{option4.name}}" stepKey="fillAdminLabel4" after="waitForOptionRow4"/> + <!--Add option 5 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption5" after="fillAdminLabel4"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('5')}}" time="30" stepKey="waitForOptionRow5" after="clickAddOption5"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('4')}}" userInput="{{option5.name}}" stepKey="fillAdminLabel5" after="waitForOptionRow5"/> + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveAttribute" after="fillAdminLabel5"/> + <waitForPageLoad stepKey="waitForSavingAttribute"/> + <see userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 0ac1914040d25..5a172ca5eabdf 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -130,4 +130,176 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> </actionGroup> + + <actionGroup name="createConfigurationsForAttributeWithImages" extends="generateConfigurationsByAttributeCode"> + <arguments> + <argument name="attributeCode" type="string" defaultValue="SomeString"/> + <argument name="image" defaultValue="ProductImage"/> + </arguments> + + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleSetOfImages}}" stepKey="clickOnApplySingleImageSetToAllSku" after="enterAttributeQuantity"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageUploadButton}}" stepKey="seeImageSectionIsReady" after="clickOnApplySingleImageSetToAllSku"/> + <attachFile selector="{{AdminCreateProductConfigurationsPanel.imageFileUpload}}" userInput="{{image.file}}" stepKey="uploadFile" after="seeImageSectionIsReady"/> + <waitForElementNotVisible selector="{{AdminCreateProductConfigurationsPanel.uploadProgressBar}}" stepKey="waitForUpload" after="uploadFile"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageFile(image.fileName)}}" stepKey="waitForThumbnail" after="waitForUpload"/> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2" after="clickOnNextButton4"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup" after="clickOnSaveButton2"/> + </actionGroup> + + <actionGroup name="createConfigurationsForTwoAttribute" extends="generateConfigurationsByAttributeCode"> + <arguments> + <argument name="secondAttributeCode" type="string"/> + </arguments> + <remove keyForRemoval="clickOnSelectAll"/> + <remove keyForRemoval="clickFilters"/> + <remove keyForRemoval="fillFilterAttributeCodeField"/> + <remove keyForRemoval="clickApplyFiltersButton"/> + <remove keyForRemoval="clickOnFirstCheckbox"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(attributeCode)}}" stepKey="clickOnFirstAttributeCheckbox" after="clickCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(secondAttributeCode)}}" stepKey="clickOnSecondAttributeCheckbox" after="clickOnFirstAttributeCheckbox"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(attributeCode)}}" stepKey="grabFirstAttributeDefaultLabel" after="clickOnSecondAttributeCheckbox"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(secondAttributeCode)}}" stepKey="grabSecondAttributeDefaultLabel" after="grabFirstAttributeDefaultLabel"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabFirstAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForFirstAttribute" after="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabSecondAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForSecondAttribute" after="clickOnSelectAllForFirstAttribute"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + </actionGroup> + + <actionGroup name="saveConfiguredProduct"> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <actionGroup name="addNewProductConfigurationAttribute"> + <arguments> + <argument name="attribute" type="entity"/> + <argument name="firstOption" type="entity"/> + <argument name="secondOption" type="entity"/> + </arguments> + <!-- Create new attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{attribute.default_label}}" stepKey="fillDefaultLabel"/> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <waitForPageLoad stepKey="waitForFilters"/> + <!-- Find created below attribute and add option; save attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{attribute.default_label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateFirstNewValue"/> + <fillField userInput="{{firstOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateSecondNewValue"/> + <fillField userInput="{{secondOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnSecondNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnThirdNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFourthNextButton"/> + </actionGroup> + + <actionGroup name="changeProductConfigurationsInGrid"> + <arguments> + <argument name="firstOption" type="entity"/> + <argument name="secondOption" type="entity"/> + </arguments> + <fillField userInput="{{firstOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(firstOption.name)}}" stepKey="fillFieldNameForFirstAttributeOption"/> + <fillField userInput="{{secondOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(secondOption.name)}}" stepKey="fillFieldNameForSecondAttributeOption"/> + <fillField userInput="{{firstOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(firstOption.name)}}" stepKey="fillFieldSkuForFirstAttributeOption"/> + <fillField userInput="{{secondOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(secondOption.name)}}" stepKey="fillFieldSkuForSecondAttributeOption"/> + <fillField userInput="{{firstOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(firstOption.name)}}" stepKey="fillFieldPriceForFirstAttributeOption"/> + <fillField userInput="{{secondOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(secondOption.name)}}" stepKey="fillFieldPriceForSecondAttributeOption"/> + <fillField userInput="{{firstOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(firstOption.name)}}" stepKey="fillFieldQuantityForFirstAttributeOption"/> + <fillField userInput="{{secondOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(secondOption.name)}}" stepKey="fillFieldQuantityForSecondAttributeOption"/> + <fillField userInput="{{firstOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(firstOption.name)}}" stepKey="fillFieldWeightForFirstAttributeOption"/> + <fillField userInput="{{secondOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(secondOption.name)}}" stepKey="fillFieldWeightForSecondAttributeOption"/> + </actionGroup> + + <actionGroup name="changeProductConfigurationsInGridExceptSku" extends="changeProductConfigurationsInGrid"> + <remove keyForRemoval="fillFieldSkuForFirstAttributeOption"/> + <remove keyForRemoval="fillFieldSkuForSecondAttributeOption"/> + </actionGroup> + + <actionGroup name="addProductToConfigurationsGrid"> + <arguments> + <argument name="sku" type="string"/> + <argument name="name" type="string"/> + </arguments> + <click selector="{{AdminProductFormConfigurationsSection.actionsBtnByProductName(name)}}" stepKey="clickToExpandFirstActions"/> + <click selector="{{AdminProductFormConfigurationsSection.addProduct(name)}}" stepKey="clickChooseFirstDifferentProduct"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <waitForPageLoad stepKey="waitForFilters"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{AdminProductGridFilterSection.firstRowBySku(sku)}}" stepKey="clickOnFirstRow"/> + </actionGroup> + + <actionGroup name="addUniqueImageToConfigurableProductOption"> + <arguments> + <argument name="image" defaultValue="ProductImage"/> + <argument name="frontend_label" type="string"/> + <argument name="label" type="string"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> + <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanel.selectImagesButton}}" stepKey="selectOption"/> + <attachFile selector="{{AdminCreateProductConfigurationsPanel.uploadImagesButton(label)}}" userInput="{{image.file}}" stepKey="uploadFile"/> + <waitForElementNotVisible selector="{{AdminCreateProductConfigurationsPanel.uploadProgressBar}}" stepKey="waitForUpload"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageFile(image.fileName)}}" stepKey="waitForThumbnail"/> + </actionGroup> + + <actionGroup name="addUniquePriceToConfigurableProductOption"> + <arguments> + <argument name="frontend_label" type="string"/> + <argument name="label" type="string"/> + <argument name="price" type="string"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesToEachSkus}}" stepKey="clickOnApplyUniquePricesToEachSku"/> + <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanel.selectPriceButton}}" stepKey="selectOption"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.price(label)}}" userInput="{{price}}" stepKey="enterAttributeQuantity"/> + </actionGroup> + + <actionGroup name="saveConfigurableProductWithNewAttributeSet"> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveConfigurableProduct"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" time="30" stepKey="waitForAttributeSetConfirmation"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.addNewAttrSet}}" stepKey="clickAddNewAttributeSet"/> + <fillField selector="{{AdminChooseAffectedAttributeSetPopup.createNewAttrSetName}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillFieldNewAttrSetName"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickConfirmAttributeSet"/> + <see selector="You saved the product" stepKey="seeConfigurableSaveConfirmation" after="clickConfirmAttributeSet"/> + </actionGroup> + + <actionGroup name="saveConfigurableProductAddToCurrentAttributeSet"> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <actionGroup name="assertConfigurableProductOnAdminProductPage"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <seeInField userInput="{{ApiConfigurableProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="seeNameRequired"/> + <seeInField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="seeSkuRequired"/> + <dontSeeInField userInput="{{ApiConfigurableProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="dontSeePriceRequired"/> + </actionGroup> + + <!--Click in Next Step and see Title--> + <actionGroup name="AdminConfigurableWizardMoveToNextStepActionGroup"> + <arguments> + <argument name="title" type="string"/> + </arguments> + <click selector="{{ConfigurableProductSection.nextButton}}" stepKey="clickNextButton"/> + <waitForPageLoad stepKey="waitForNextStepLoaded"/> + <see userInput="{{title}}" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml index 033e6757c3bf9..5feaab40a7695 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminCreateApiConfigurableProductActionGroup"> <arguments> - <argument name="productName" defaultValue="ApiConfigurableProductWithOutCategory" type="string"/> + <argument name="productName" defaultValue="{{ApiConfigurableProductWithOutCategory.name}}" type="string"/> </arguments> <!-- Create the configurable product based on the data in the /data folder --> @@ -62,4 +62,17 @@ <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> </actionGroup> + + <!-- Create the configurable product, children are not visible individually --> + <actionGroup name="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwoHidden" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml index 95533057608f2..c4ad02ee14134 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml @@ -7,8 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="GotoCatalogProductsPage"> <!--Click on Catalog item--> @@ -168,5 +167,4 @@ <waitForPageLoad stepKey="waitForAllFilterReset"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 39c206e365a2d..e2759fc0fd23d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -21,4 +21,15 @@ <!-- @TODO: MAGETWO-80272 Move to Magento_Checkout --> <seeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="AssertAddToCart" /> </actionGroup> + + <!-- Check configurable product out of stock on the category page --> + <actionGroup name="StorefrontCheckCategoryOutOfStockConfigurableProduct"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName(product.name)}}" stepKey="assertProductName"/> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> + <seeElement selector="{{StorefrontCategoryProductSection.ProductStockUnavailable}}" stepKey="AssertOutOfStock"/> + <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="AssertAddToCart" /> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml index 0a8d8e56426ba..2c3e5716d6add 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -24,4 +24,83 @@ <see userInput="{{product.custom_attributes[description]}}" selector="{{StorefrontProductInfoMainSection.productDescription}}" stepKey="assertProductDescription"/> <see userInput="{{product.custom_attributes[short_description]}}" selector="{{StorefrontProductInfoMainSection.productShortDescription}}" stepKey="assertProductShortDescription"/> </actionGroup> -</actionGroups> \ No newline at end of file + + <!-- Check Storefront Configurable Product Option --> + <actionGroup name="VerifyOptionInProductStorefront"> + <arguments> + <argument name="attributeCode" type="string"/> + <argument name="optionName" type="string"/> + </arguments> + <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID(attributeCode, optionName)}}" stepKey="verifyOptionExists"/> + </actionGroup> + + <!-- Adds Single Option Configurable Product to cart--> + <actionGroup name="SelectSingleAttributeAndAddToCart"> + <arguments> + <argument name="productName" type="string"/> + <argument name="attributeCode" type="string"/> + <argument name="optionName" type="string"/> + </arguments> + <selectOption selector="{{StorefrontProductInfoMainSection.attributeSelectByAttributeID(attributeCode)}}" userInput="{{optionName}}" stepKey="selectAttribute"/> + <click stepKey="addProduct" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> + </actionGroup> + + <!-- Verify configurable product options in storefront product view --> + <actionGroup name="storefrontCheckConfigurableProductOptions"> + <arguments> + <argument name="product" type="entity"/> + <argument name="firstOption" type="entity"/> + <argument name="secondOption" type="entity"/> + </arguments> + <selectOption userInput="{{firstOption.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="seeConfigurableProductName"/> + <see userInput="{{firstOption.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPricePresent"/> + <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="seeConfigurableProductSku"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertInStock"/> + <see userInput="{{colorProductAttribute.default_label}}" selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" stepKey="seeColorAttributeName"/> + <dontSee userInput="As low as" selector="{{StorefrontProductInfoMainSection.productPriceLabel}}" stepKey="dontSeeProductPriceLabel1"/> + <selectOption userInput="{{secondOption.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption2"/> + <dontSee userInput="As low as" selector="{{StorefrontProductInfoMainSection.productPriceLabel}}" stepKey="dontSeeProductPriceLabel2"/> + <see userInput="{{secondOption.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="seeProductPrice2"/> + </actionGroup> + + <!-- Assert option image in storefront product page --> + <actionGroup name="assertOptionImageInStorefrontProductPage"> + <arguments> + <argument name="product" type="entity"/> + <argument name="label" type="string"/> + <argument name="image" defaultValue="MagentoLogo"/> + </arguments> + <seeInCurrentUrl url="/{{product.urlKey}}.html" stepKey="checkUrl"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile(image.filename)}}" stepKey="seeFirstImage"/> + </actionGroup> + + <!-- Assert option image and price in storefront product page --> + <actionGroup name="AssertOptionImageAndPriceInStorefrontProductActionGroup"> + <arguments> + <argument name="label" type="string"/> + <argument name="image" type="string"/> + <argument name="price" type="string"/> + </arguments> + <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile(image)}}" stepKey="seeImage"/> + <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.price}}" stepKey="seeProductPrice"/> + </actionGroup> + + <!-- Assert configurable product with special price in storefront product page --> + <actionGroup name="assertConfigurableProductWithSpecialPriceOnStorefrontProductPage"> + <arguments> + <argument name="option" type="string"/> + <argument name="price" type="string"/> + <argument name="specialPrice" defaultValue="specialProductPrice"/> + </arguments> + <selectOption userInput="{{option}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionWithSpecialPrice"/> + <see userInput="{{specialProductPrice.price}}" selector="{{StorefrontProductInfoMainSection.productSpecialPrice}}" stepKey="seeSpecialProductPrice"/> + <see userInput="Regular Price" selector="{{StorefrontProductInfoMainSection.specialProductText}}" stepKey="seeText"/> + <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.oldProductPrice}}" stepKey="seeOldProductPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml new file mode 100644 index 0000000000000..7780827381533 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.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"> + <!-- Check configurable product attribute options on the category page --> + <actionGroup name="SelectStorefrontSideBarAttributeOption"> + <arguments> + <argument name="categoryName" type="string"/> + <argument name="attributeDefaultLabel" type="string"/> + </arguments> + <amOnPage url="{{categoryName}}" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="seeCategoryInFrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" stepKey="clickOnCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad1"/> + <seeElement selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeDefaultLabel)}}" stepKey="seeAttributeOptionsTitle"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeDefaultLabel)}}" stepKey="clickAttributeOptions"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 0000000000000..f3b0786236062 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ +<?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="VerifyProductTypeOrder"> + <seeElement stepKey="seeConfigurableInOrder" selector="{{AdminProductDropdownOrderSection.configurableProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml index 73a668fd2fefd..0018f5996c9bc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="NewProductsData" type="user"> <data key="productName" unique="prefix">Shoes</data> <data key="price">60</data> @@ -31,5 +31,4 @@ <data key="configurableProduct">configurable</data> <data key="errorMessage">element.disabled is not a function</data> </entity> - </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml index f231d74b70dad..a1a499f33eda0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml @@ -8,6 +8,11 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ConfigurableProductOneOption" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + </entity> <entity name="ConfigurableProductTwoOptions" type="ConfigurableProductOption"> <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> <data key="label">option</data> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml index 9342172f7d4df..4c5f83ecebecf 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml @@ -42,4 +42,22 @@ <data key="name">Black</data> <data key="price">5.00</data> </entity> + <entity name="colorConfigurableProductAttribute1" type="product_attribute"> + <data key="name" unique="suffix">Green</data> + <data key="sku" unique="suffix">sku-green</data> + <data key="type_id">simple</data> + <data key="price">1</data> + <data key="visibility">1</data> + <data key="quantity">1</data> + <data key="weight">1</data> + </entity> + <entity name="colorConfigurableProductAttribute2" type="product_attribute"> + <data key="name" unique="suffix">Red</data> + <data key="sku" unique="suffix">sku-red</data> + <data key="type_id">simple</data> + <data key="price">2</data> + <data key="visibility">1</data> + <data key="quantity">10</data> + <data key="weight">1</data> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml index 4289638352990..78e4c7bced8e2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml @@ -10,5 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminChooseAffectedAttributeSetPopup"> <element name="confirm" type="button" selector="button[data-index='confirm_button']" timeout="30"/> + <element name="addNewAttrSet" type="radio" selector="//input[@data-index='affectedAttributeSetNew']" timeout="30"/> + <element name="createNewAttrSetName" type="input" selector="//input[@name='configurableNewAttributeSetName']" timeout="30"/> + <element name="closePopUp" type="button" selector="//*[contains(@class,'product_form_product_form_configurable_attribute_set')]//button[@data-role='closeBtn']" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index 7901b6f2290c9..a5e74145c9fec 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -16,6 +16,8 @@ <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> <element name="id" type="text" selector="//tr[contains(@data-repeat-index, '0')]/td[2]/div"/> + <element name="attributeCheckbox" type="checkbox" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//input[@data-action='select-row']" parameterized="true"/> + <element name="defaultLabel" type="text" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//td[3]/div[@class='data-grid-cell-content']" parameterized="true"/> <element name="selectAll" type="button" selector=".action-select-all"/> <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> @@ -24,6 +26,10 @@ <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> <element name="attributeCheckboxByIndex" type="input" selector="li.attribute-option:nth-of-type({{var1}}) input" parameterized="true"/> + <element name="applySingleSetOfImages" type="radio" selector=".admin__field-label[for='apply-single-set-radio']" timeout="30"/> + <element name="imageFileUpload" type="input" selector=".steps-wizard-section input[type='file'][name='image']"/> + <element name="imageUploadButton" type="button" selector=".steps-wizard-section div.gallery"/> + <element name="applyUniquePricesByAttributeToEachSku" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']"/> <element name="applySinglePriceToAllSkus" type="radio" selector=".admin__field-label[for='apply-single-price-radio']"/> <element name="singlePrice" type="input" selector="#apply-single-price-input"/> @@ -33,8 +39,18 @@ <element name="attribute3" type="input" selector="#apply-single-price-input-2"/> <element name="applySingleQuantityToEachSkus" type="radio" selector=".admin__field-label[for='apply-single-inventory-radio']" timeout="30"/> + <element name="applyUniqueImagesToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-images-radio']" timeout="30"/> + <element name="applyUniquePricesToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']" timeout="30"/> + <element name="selectImagesButton" type="select" selector="#apply-images-attributes" timeout="30"/> + <element name="uploadImagesButton" type="file" selector="//*[text()='{{option}}']/../../div[@data-role='gallery']//input[@type='file']" timeout="30" parameterized="true"/> + <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> + <element name="imageFile" type="text" selector="//*[@data-role='gallery']//img[contains(@src, '{{url}}')]" parameterized="true"/> + <element name="selectPriceButton" type="select" selector="#select-each-price" timeout="30"/> + <element name="price" type="input" selector="//*[text()='{{option}}']/../..//input[contains(@id, 'apply-single-price-input')]" parameterized="true"/> <element name="quantity" type="input" selector="#apply-single-inventory-input"/> <element name="gridLoadingMask" type="text" selector="[data-role='spinner'][data-component*='product_attributes_listing']"/> + <element name="attributeCheckboxByName" type="input" selector="//*[contains(@data-attribute-option-title,'{{arg}}')]//input[@type='checkbox']" parameterized="true"/> <element name="attributeColorCheckbox" type="select" selector="//div[contains(text(),'color') and @class='data-grid-cell-content']/../preceding-sibling::td/label/input"/> + <element name="attributeRowByAttributeCode" type="block" selector="//td[count(../../..//th[./*[.='Attribute Code']]/preceding-sibling::th) + 1][./*[.='{{attribute_code}}']]/../td//input[@data-action='select-row']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml index 44077888f8bc0..658e7a5fec9b3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -9,6 +9,15 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewAttributePanel"> + <element name="useInSearch" type="select" selector="#is_searchable"/> + <element name="visibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> + <element name="comparableOnStorefront" type="select" selector="#is_comparable"/> + <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + <element name="visibleOnCatalogPagesOnStorefront" type="select" selector="#is_visible_on_front"/> + <element name="useInProductListing" type="select" selector="#used_in_product_listing"/> + <element name="usedForStoringInProductListing" type="select" selector="#used_for_sort_by"/> + <element name="storefrontPropertiesTab" selector="#front_fieldset-wrapper"/> + <element name="storefrontPropertiesTitle" selector="//span[text()='Storefront Properties']"/> <element name="container" type="text" selector="#create_new_attribute"/> <element name="saveAttribute" type="button" selector="#save"/> <element name="newAttributeIFrame" type="iframe" selector="create_new_attribute_container"/> @@ -20,5 +29,6 @@ <element name="optionAdminValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][0]']" parameterized="true"/> <element name="optionDefaultStoreValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][1]']" parameterized="true"/> <element name="deleteOption" type="button" selector="#delete_button_option_{{row}}" parameterized="true"/> + <element name="deleteOptionByName" type="button" selector="//*[contains(@value, '{{arg}}')]/../following-sibling::td[contains(@id, 'delete_button_container')]/button" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 0000000000000..6056e7f3cbd09 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.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="AdminProductDropdownOrderSection"> + <element name="configurableProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Virtual Product']]) and not(preceding-sibling::li[span[@title='Grouped Product']]) and not(preceding-sibling::li[span[@title='Bundle Product']]) and not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']])]/span[@title='Configurable Product']"/> + </section> +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml index f2caef1717e84..f6828a3b86312 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormConfigurationsSection"> <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> + <element name="createdConfigurationsBlock" type="text" selector="div.admin__field.admin__field-wide"/> <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> <element name="currentVariationsRows" type="button" selector=".data-row"/> <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> @@ -18,12 +19,25 @@ <element name="currentVariationsQuantityCells" type="textarea" selector=".admin__control-fields[data-index='quantity_container']"/> <element name="currentVariationsAttributesCells" type="textarea" selector=".admin__control-fields[data-index='attributes']"/> <element name="currentVariationsStatusCells" type="textarea" selector="._no-header[data-index='status']"/> + <element name="firstSKUInConfigurableProductsGrid" type="input" selector="//input[@name='configurable-matrix[0][sku]']"/> <element name="actionsBtn" type="button" selector="(//button[@class='action-select']/span[contains(text(), 'Select')])[{{var1}}]" parameterized="true"/> + <element name="actionsBtnByProductName" type="textarea" selector="//*[.='Attributes']/ancestor::tr/td[@data-index='attributes']//span[contains(text(), '{{var}}')]/ancestor::tr//button[@class='action-select']" parameterized="true"/> + <element name="addProduct" type="button" selector="//*[.='Attributes']/ancestor::tr/td[@data-index='attributes']//span[contains(text(), '{{var}}')]/ancestor::tr//a[text()='Choose a different Product']" parameterized="true"/> <element name="removeProductBtn" type="button" selector="//a[text()='Remove Product']"/> <element name="disableProductBtn" type="button" selector="//a[text()='Disable Product']"/> <element name="enableProductBtn" type="button" selector="//a[text()='Enable Product']"/> + <element name="confProductSku" type="input" selector="//*[@name='configurable-matrix[{{arg}}][sku]']" parameterized="true"/> + <element name="confProductNameCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='name_container']//input" parameterized="true"/> + <element name="confProductSkuCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='sku_container']//input" parameterized="true"/> + <element name="confProductPriceCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='price_container']//input" parameterized="true"/> + <element name="confProductQuantityCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='quantity_container']//input" parameterized="true"/> + <element name="confProductWeightCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='price_weight']//input" parameterized="true"/> + <element name="confProductSkuMessage" type="text" selector="//*[@name='configurable-matrix[{{arg}}][sku]']/following-sibling::label" parameterized="true"/> <element name="variationsSkuInputByRow" selector="[data-index='configurable-matrix'] table > tbody > tr:nth-of-type({{row}}) input[name*='sku']" type="input" parameterized="true"/> <element name="variationsSkuInputErrorByRow" selector="[data-index='configurable-matrix'] table > tbody > tr:nth-of-type({{row}}) .admin__field-error" type="text" parameterized="true"/> + <element name="variationLabel" type="text" selector="//div[@data-index='configurable-matrix']/label"/> + <element name="stepsWizardTitle" type="text" selector="div.content:not([style='display: none;']) .steps-wizard-title"/> + <element name="attributeEntityByName" type="text" selector="//div[@class='attribute-entity']//div[normalize-space(.)='{{attributeLabel}}']" parameterized="true"/> </section> <section name="AdminConfigurableProductFormSection"> <element name="productWeight" type="input" selector=".admin__control-text[name='product[weight]']"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml index b3077d9d5d566..ea5638f6816c9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CatalogProductsSection"> <element name="catalogItem" type="button" selector="//*[@id='menu-magento-catalog-catalog']/a/span"/> <element name="productItem" type="button" selector="//*[@data-ui-id='menu-magento-catalog-catalog-products']/a"/> @@ -53,7 +53,6 @@ <element name="saveAttributeButton" type="button" selector="//*[@id='save']"/> <element name="advancedAttributeProperties" type="button" selector="//*[@id='advanced_fieldset-wrapper']//*[contains(text(),'Advanced Attribute Properties')]"/> <element name="attributeCodeField" type="input" selector="//*[@id='attribute_code']"/> - </section> <section name="CreateProductConfigurations"> @@ -64,5 +63,4 @@ <element name="checkboxBlack" type="input" selector="//fieldset[@class='admin__fieldset admin__fieldset-options']//*[contains(text(),'black')]/preceding-sibling::input"/> <element name="errorMessage" type="input" selector="//div[@data-ui-id='messages-message-error']"/> </section> - </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index b195c19f7bedd..24cd9262b6742 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -11,11 +11,14 @@ <section name="StorefrontProductInfoMainSection"> <element name="optionByAttributeId" type="input" selector="#attribute{{var1}}" parameterized="true"/> <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> + <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> <element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/> <element name="productAttributeOptionsError" type="text" selector="//div[@class='mage-error']"/> <!-- Parameter is the order number of the attribute on the page (1 is the newest) --> <element name="nthAttributeOnPage" type="block" selector="tr:nth-of-type({{numElement}}) .data" parameterized="true"/> <element name="stockIndication" type="block" selector=".stock" /> + <element name="attributeSelectByAttributeID" type="select" selector="//div[@class='fieldset']//div[//span[text()='{{attribute_code}}']]//select" parameterized="true"/> + <element name="attributeOptionByAttributeID" type="select" selector="//div[@class='fieldset']//div[//span[text()='{{attribute_code}}']]//option[text()='{{optionName}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml new file mode 100644 index 0000000000000..52443a17dfe64 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -0,0 +1,124 @@ +<?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="AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Update product"/> + <title value="Adding new options with images and prices to Configurable Product"/> + <description value="Test case verifies possibility to add new options for configurable attribute for existing configurable product."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13339"/> + <group value="configurableProduct"/> + </annotations> + + <before> + <actionGroup ref="AdminCreateApiConfigurableProductActionGroup" stepKey="createConfigurableProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open edit product page--> + <amOnPage url="{{AdminProductEditPage.url($$createConfigProductCreateConfigurableProduct.id$$)}}" stepKey="goToProductEditPage"/> + + <!--Open edit configuration wizard--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickEditConfigurations"/> + <see userInput="Select Attributes" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToAttributeValuesStep"> + <argument name="title" value="Attribute Values"/> + </actionGroup> + <seeElement selector="{{AdminProductFormConfigurationsSection.attributeEntityByName($$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$)}}" stepKey="seeAttribute"/> + + <!--Create one color option via "Create New Value" link--> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue"/> + <fillField userInput="{{colorDefaultProductAttribute1.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToBulkStep"> + <argument name="title" value="Bulk Images, Price and Quantity"/> + </actionGroup> + + <!--Select Apply unique images by attribute to each SKU and color attribute in dropdown in Images--> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> + <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" + selector="{{AdminCreateProductConfigurationsPanel.selectImagesButton}}" stepKey="selectAttributeOption"/> + + <!-- Add images to configurable product attribute options --> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="ImageUpload"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="ImageUpload_1"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionThree"> + <argument name="image" value="ImageUpload3"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="{{colorDefaultProductAttribute1.name}}"/> + </actionGroup> + + <!--Add prices to configurable product attribute options--> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesToEachSkus}}" stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" + selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" stepKey="selectAttributes"/> + <fillField userInput="10" selector="{{AdminCreateProductConfigurationsPanel.price($$getConfigAttributeOption1CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice"/> + <fillField userInput="20" selector="{{AdminCreateProductConfigurationsPanel.price($$getConfigAttributeOption2CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice1"/> + <fillField userInput="30" selector="{{AdminCreateProductConfigurationsPanel.price(colorDefaultProductAttribute1.name)}}" stepKey="fillAttributePrice2"/> + + <!-- Add quantity to product attribute options --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToSummaryStep"> + <argument name="title" value="Summary"/> + </actionGroup> + + <!--Click Generate Configure button--> + <click selector="{{ConfigurableProductSection.generateConfigure}}" stepKey="clickGenerateConfigure"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to frontend and check image and price--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertFirstOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> + <argument name="image" value="{{ImageUpload.filename}}"/> + <argument name="price" value="10"/> + </actionGroup> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertSecondOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> + <argument name="image" value="{{ImageUpload_1.filename}}"/> + <argument name="price" value="20"/> + </actionGroup> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertThirdOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="{{colorDefaultProductAttribute1.name}}"/> + <argument name="image" value="{{ImageUpload3.filename}}"/> + <argument name="price" value="30"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml new file mode 100644 index 0000000000000..68bf703ecdab4 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Assert notice that existing sku automatically changed when saving product with same sku"/> + <description value="Admin should not be able to create configurable product and two new options with the same sku"/> + <testCaseId value="MC-13693"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Create product configurations--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two option --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change products sku configurations in grid --> + <fillField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(colorConfigurableProductAttribute1.name)}}" stepKey="fillFieldSkuForFirstAttributeOption"/> + <fillField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(colorConfigurableProductAttribute2.name)}}" stepKey="fillFieldSkuForSecondAttributeOption"/> + + <!-- Save product --> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + + <!-- Assert product auto incremented sku notice message; see success message --> + <see selector="{{AdminMessagesSection.noticeMessage}}" stepKey="seeNoticeMessage" userInput="SKU for product {{ApiConfigurableProduct.name}} has been changed to {{ApiConfigurableProduct.sku}}-2."/> + <see selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage" userInput="You saved the product."/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml new file mode 100644 index 0000000000000..dd641fd370ba7 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml @@ -0,0 +1,126 @@ +<?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="AdminCheckValidatorConfigurableProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check that validator works correctly when creating Configurations for Configurable Products"/> + <description value="Verify validator works correctly for Configurable Products"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95995"/> + <useCaseId value="MAGETWO-95834"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{ApiConfigurableProduct.name}}-thisIsShortName"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Find the product that we just created using the product grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductFilterLoad"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Create configurations based off the Text Swatch we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + + <!--Create new attribute--> + <waitForElementVisible stepKey="waitForNewAttributePageOpened" selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickCreateNewAttribute" after="waitForNewAttributePageOpened"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="enterAttributePanelIFrame" after="clickCreateNewAttribute"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.defaultLabel}}" time="30" stepKey="waitForIframeLoad" after="enterAttributePanelIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel" after="waitForIframeLoad"/> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="{{colorProductAttribute.input_type}}" stepKey="selectAttributeInputType" after="fillDefaultLabel"/> + <!--Add option to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1" after="selectAttributeInputType"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="ThisIsLongNameNameLengthMoreThanSixtyFourThisIsLongNameNameLength" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <fillField selector="{{AdminNewAttributePanel.optionDefaultStoreValue('0')}}" userInput="{{colorProductAttribute1.name}}" stepKey="fillDefaultLabel1" after="fillAdminLabel1"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(productDropDownAttribute.attribute_code)}}" stepKey="waitForNextPageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(productDropDownAttribute.attribute_code)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="waitForNextPageOpened2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="enterAttributePrice"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> + + <!--Close modal window--> + <click selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="clickOnClosePopup"/> + <waitForElementNotVisible selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="waitForDialogClosed"/> + + <!--See that validation message is shown under the fields--> + <scrollTo selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" stepKey="scrollTConfigurationTab"/> + <see userInput="Please enter less or equal than 64 symbols." selector="{{AdminProductFormConfigurationsSection.confProductSkuMessage('0')}}" stepKey="SeeValidationMessage"/> + + <!--Edit "SKU" with valid quantity--> + <fillField stepKey="fillValidValue" selector="{{AdminProductFormConfigurationsSection.confProductSku('0')}}" userInput="{{ApiConfigurableProduct.name}}-thisIsShortName"/> + + <!--Click on "Save"--> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + + <!--Click on "Confirm". Product is saved, success message appears --> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml index 48f46a1205ec3..2af85e1bac048 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml @@ -36,6 +36,7 @@ </actionGroup> <!-- assert color configurations on the admin create product page --> + <dontSee selector="{{AdminProductFormConfigurationsSection.variationLabel}}" stepKey="seeLabelNotVisible"/> <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="3" stepKey="seeNumberOfRows"/> <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute1.name}}" stepKey="seeAttributeName1InField"/> <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeAttributeName2InField"/> @@ -68,4 +69,71 @@ <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeInDropDown2"/> <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute3.name}}" stepKey="seeInDropDown3"/> </test> + + <test name="AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create, Read, Update, Delete"/> + <title value="admin should be able to create a configurable product after incorrect sku"/> + <description value="admin should be able to create a configurable product after incorrect sku"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96365"/> + <useCaseId value="MAGETWO-94556"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="goToEditPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPane"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="color" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue1"/> + <fillField userInput="{{colorProductAttribute2.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{ConfigurableProductSection.generateConfigure}}" stepKey="generateConfigure"/> + <waitForPageLoad stepKey="waitForGenerateConfigure"/> + <grabValueFrom selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" stepKey="grabTextFromContent"/> + <fillField stepKey="fillMoreThan64Symbols" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="01234567890123456789012345678901234567890123456789012345678901234"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct1"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" visible="true" stepKey="clickOnCloseInPopup"/> + <see stepKey="seeErrorMessage" userInput="Please enter less or equal than 64 symbols."/> + <fillField stepKey="fillCorrectSKU" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="$grabTextFromContent"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> + <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <waitForPageLoad stepKey="waitForProductAttributes"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> + <click selector="{{DropdownAttributeOptionsSection.deleteButton(1)}}" stepKey="deleteOption"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid2"/> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="$grabTextFromContent"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml index af12f49bf86ea..39aa516077c56 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml @@ -57,7 +57,13 @@ <click selector="{{AdminUpdateAttributesSection.toggleDescription}}" stepKey="clickToggleDescription"/> <fillField selector="{{AdminUpdateAttributesSection.description}}" userInput="MFTF automation!" stepKey="fillDescription"/> <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 3 record(s) were updated." stepKey="seeSaveSuccess"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Check storefront for description --> <amOnPage url="$$createProduct1.sku$$.html" stepKey="gotoProduct1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml new file mode 100755 index 0000000000000..93df31a7d89e5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -0,0 +1,202 @@ +<?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="AdminCreateConfigurableProductSwitchToSimpleTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from configurable to simple"/> + <description value="After selecting a configurable product when adding Admin should be switch to simple implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10926"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="configurable"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + </test> + <test name="AdminCreateConfigurableProductSwitchToVirtualTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from configurable to virtual"/> + <description value="After selecting a configurable product when adding Admin should be switch to virtual implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10927"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="configurable"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> + </test> + <test name="AdminCreateVirtualProductSwitchToConfigurableTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from virtual to configurable"/> + <description value="After selecting a virtual product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10930"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MSI-2110"/> + </skip> + </annotations> + <before> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + </after> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="virtual"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> + <test name="AdminCreateSimpleProductSwitchToConfigurableTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from simple to configurable"/> + <description value="After selecting a simple product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <useCaseId value="MAGETWO-44165"/> + <testCaseId value="MAGETWO-29398"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MSI-2110"/> + </skip> + </annotations> + <before> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + </after> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="simple"/> + </actionGroup> + <!-- Create configurable product from simple product page--> + <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <!-- Verify product on store front --> + <comment userInput="Verify product on store front" stepKey="commentVerifyProductGrid"/> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> + <test name="AdminCreateDownloadableProductSwitchToConfigurableTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from downloadable to configurable"/> + <description value="After selecting a downloadable product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <useCaseId value="MAGETWO-44165"/> + <testCaseId value="MAGETWO-29398"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MSI-2110"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create configurable product from downloadable product page--> + <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <!-- Open Dropdown and select downloadable product option --> + <comment stepKey="beforeOpenProductFillForm" userInput="Selecting Product from the Add Product Dropdown"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="downloadable"/> + </actionGroup> + <scrollTo selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="scrollToDownloadableInfo" /> + <uncheckOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> + <!-- Fill form for Downloadable Product Type --> + <comment stepKey="beforeFillProductForm" userInput="Filling Product Form"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="SetProductUrlKey" stepKey="setProductUrl"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <!-- Check that product was added with implicit type change --> + <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <actionGroup ref="filterProductGridByName" stepKey="searchForProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefrontProductPage"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml new file mode 100644 index 0000000000000..f4f607e9119b6 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml @@ -0,0 +1,82 @@ +<?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="AdminCreateConfigurableProductBasedOnParentSkuTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Configurable product variation's sku should be based on parent SKU"/> + <description value="Admin should be able to create configurable product with two new options based on parent SKU, without assigned to category and attribute set"/> + <testCaseId value="MC-13689"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product with children products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteProducts"> + <argument name="sku" value="{{ApiConfigurableProduct.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Create product configurations--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two option --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change product configurations except sku --> + <actionGroup ref="changeProductConfigurationsInGridExceptSku" stepKey="changeProductConfigurationsInGridExceptSku"> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveConfigurableProductAddToCurrentAttributeSet" stepKey="saveProduct"/> + + <!-- Assert child products generated sku in grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> + <actionGroup ref="filterProductGridByName2" stepKey="filterFirstProductByNameInGrid"> + <argument name="name" value="{{colorConfigurableProductAttribute1.name}}"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute1.name}}" stepKey="seeFirstProductSkuInGrid"/> + <actionGroup ref="filterProductGridByName2" stepKey="filterSecondProductByNameInGrid"> + <argument name="name" value="{{colorConfigurableProductAttribute2.name}}"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute2.name}}" stepKey="seeSecondProductSkuInGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml new file mode 100644 index 0000000000000..a7242b43c2b5f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml @@ -0,0 +1,124 @@ +<?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="AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with creating new category and new attribute (required fields only)"/> + <description value="Admin should be able to create configurable product with creating new category and new attribute (required fields only)"/> + <testCaseId value="MC-13687"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete children products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> + </actionGroup> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> + <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product required fields only--> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{ApiConfigurableProduct.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{ApiConfigurableProduct.price}}" stepKey="fillProductPrice"/> + + <!-- Add configurable product in category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!--Create product configurations--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two option --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change product configurations in grid --> + <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Save configurable product; add product to new attribute set --> + <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> + + <!-- Find configurable product in grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Assert configurable product on admin product page --> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="assertConfigurableProductOnAdminProductPage" stepKey="assertConfigurableProductOnAdminProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Assert configurable product in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + </actionGroup> + + <!--Assert configurable product on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml new file mode 100644 index 0000000000000..49f3f8b5ea931 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml @@ -0,0 +1,123 @@ +<?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="AdminCreateConfigurableProductWithDisabledChildrenProductsTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with disabled children products"/> + <description value="Admin should be able to create configurable product with disabled children products, assigned to category"/> + <testCaseId value="MC-13711"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create attribute with one options --> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the child that will be a part of the configurable product --> + <createData entity="SimpleProductOffline" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Don't display out of stock product --> + <actionGroup ref="noDisplayOutOfStockProduct" stepKey="revertDisplayOutOfStockProduct"/> + + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete created data --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations and add attribute and select all options --> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + + <!-- Add configurable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> + + <!-- Add child product to configurations grid --> + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSimpleProduct"> + <argument name="sku" value="$$createSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOption.option[store_labels][1][label]$$"/> + </actionGroup> + + <!-- Save configurable product --> + <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> + + <!-- Find configurable product in grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Assert configurable product on admin product page --> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="assertConfigurableProductOnAdminProductPage" stepKey="assertConfigurableProductOnAdminProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Assert configurable attributes block is present on product page --> + <scrollTo selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" stepKey="scrollToSearchEngineTab" /> + <seeElement selector="{{AdminProductFormConfigurationsSection.createdConfigurationsBlock}}" stepKey="seeCreatedConfigurations"/> + <see userInput="$$createSimpleProduct.name$$" selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" stepKey="seeProductNameInConfigurations"/> + + <!-- Display out of stock product --> + <actionGroup ref="displayOutOfStockProduct" stepKey="displayOutOfStockProduct"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Assert configurable product is not present in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + + <!-- Assert configurable product is out of stock--> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml new file mode 100644 index 0000000000000..925e7a890cead --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -0,0 +1,159 @@ +<?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="AdminCreateConfigurableProductWithImagesTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with images"/> + <description value="Admin should be able to create configurable product with images"/> + <testCaseId value="MC-13713"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create first attribute with 2 options --> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createFirstConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + </createData> + + <!-- Create second attribute with 2 options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createSecondConfigProductAttribute"/> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOptionThree"> + <requiredEntity createDataKey="createSecondConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption4" stepKey="createConfigProductAttributeOptionFour"> + <requiredEntity createDataKey="createSecondConfigProductAttribute"/> + </createData> + + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + <requiredEntity createDataKey="createSecondConfigProductAttribute"/> + </createData> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete created data --> + <deleteData createDataKey="createFirstConfigProductAttribute" stepKey="deleteFirstConfigProductAttribute"/> + <deleteData createDataKey="createSecondConfigProductAttribute" stepKey="deleteSecondConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Add configurable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> + + <!-- Add image to product --> + <actionGroup ref="addProductImage" stepKey="addImageForProduct"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!-- Create product configurations --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="addImageForProduct"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!-- Show 100 attributes per page --> + <actionGroup ref="adminDataGridSelectPerPage" stepKey="selectNumberOfAttributesPerPage"> + <argument name="perPage" value="100"/> + </actionGroup> + + <!--Add attributes and select all options --> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeRowByAttributeCode($$createFirstConfigProductAttribute.attribute_code$$)}}" stepKey="clickOnFirstAttributeCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeRowByAttributeCode($$createSecondConfigProductAttribute.attribute_code$$)}}" stepKey="clickOnSecondAttributeCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute($$createFirstConfigProductAttribute.default_frontend_label$$)}}" stepKey="clickOnSelectAllInFirstAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute($$createSecondConfigProductAttribute.default_frontend_label$$)}}" stepKey="clickOnSelectAllInSecondAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + + <!-- Add images to first product attribute options --> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="MagentoLogo"/> + <argument name="frontend_label" value="$$createFirstConfigProductAttribute.default_frontend_label$$"/> + <argument name="label" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="TestImageNew"/> + <argument name="frontend_label" value="$$createFirstConfigProductAttribute.default_frontend_label$$"/> + <argument name="label" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> + </actionGroup> + + <!-- Add price to second product attribute options --> + <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceToConfigurableProductOptionThree"> + <argument name="frontend_label" value="$$createSecondConfigProductAttribute.default_frontend_label$$"/> + <argument name="label" value="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$"/> + <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> + </actionGroup> + <actionGroup ref="addUniquePriceToConfigurableProductOption" stepKey="addPriceToConfigurableProductOptionFour"> + <argument name="frontend_label" value="$$createSecondConfigProductAttribute.default_frontend_label$$"/> + <argument name="label" value="$$createConfigProductAttributeOptionFour.option[store_labels][1][label]$$"/> + <argument name="price" value="{{virtualProductWithRequiredFields.price}}"/> + </actionGroup> + + <!-- Add quantity to product attribute options --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + + <!-- Save product --> + <actionGroup ref="saveConfigurableProductAddToCurrentAttributeSet" stepKey="saveProduct"/> + + <!-- Assert configurable product in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="virtualProductWithRequiredFields"/> + </actionGroup> + + <!-- Assert product image in storefront product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertProductImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!-- Assert product options images in storefront product page --> + <actionGroup ref="assertOptionImageInStorefrontProductPage" stepKey="assertFirstOptionImageInStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="label" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <actionGroup ref="assertOptionImageInStorefrontProductPage" stepKey="assertSecondOptionImageInStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="label" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> + <argument name="image" value="TestImageNew"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml new file mode 100644 index 0000000000000..0b83fdc1788d3 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml @@ -0,0 +1,140 @@ +<?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="AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create Configurable Product with three product, display out of stock products"/> + <description value="Admin should be able to create Configurable Product with one out of stock and several in stock options, display out of stock products"/> + <testCaseId value="MC-13714"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create attribute with 3 options to be used in children products --> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOptionThree"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOptionThree"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 3 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionOne"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> + </createData> + <createData entity="ApiSimpleOutOfStock" stepKey="createSimpleOutOfStockProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionThree"/> + </createData> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Don't display out of stock product --> + <actionGroup ref="noDisplayOutOfStockProduct" stepKey="revertDisplayOutOfStockProduct"/> + + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete created data --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createSimpleOutOfStockProduct" stepKey="deleteSimpleOutOfStockProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations and add attribute and select all options --> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + + <!-- Add configurable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> + + <!-- Add child products to configurations grid --> + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addFirstSimpleProduct"> + <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> + </actionGroup> + + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSecondSimpleProduct"> + <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> + </actionGroup> + + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addOutOfStockProduct"> + <argument name="sku" value="$$createSimpleOutOfStockProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$"/> + </actionGroup> + + <!-- Save configurable product --> + <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> + + <!-- Display out of stock product --> + <actionGroup ref="displayOutOfStockProduct" stepKey="displayOutOfStockProduct"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Assert configurable product in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="$$createFirstSimpleProduct$$"/> + </actionGroup> + + <!-- Assert out of stock option is absent on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <dontSee userInput="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="assertOptionNotAvailable" /> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml new file mode 100644 index 0000000000000..e24ac07c30d1e --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml @@ -0,0 +1,134 @@ +<?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="AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create Configurable Product with three product, don't display out of stock products"/> + <description value="Admin should be able to create Configurable Product with one out of stock and several in stock options, don't display out of stock products"/> + <testCaseId value="MC-13715"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create attribute with 3 options to be used in children products --> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOptionThree"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOptionThree"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 3 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionOne"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> + </createData> + <createData entity="ApiSimpleOutOfStock" stepKey="createSimpleOutOfStockProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionThree"/> + </createData> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete created data --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createSimpleOutOfStockProduct" stepKey="deleteSimpleOutOfStockProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations and add attribute and select all options --> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + + <!-- Add configurable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> + + <!-- Add child products to configurations grid --> + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addFirstSimpleProduct"> + <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> + </actionGroup> + + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSecondSimpleProduct"> + <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> + </actionGroup> + + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addOutOfStockProduct"> + <argument name="sku" value="$$createSimpleOutOfStockProduct.sku$$"/> + <argument name="name" value="$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$"/> + </actionGroup> + + <!-- Save configurable product --> + <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Assert configurable product in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="$$createFirstSimpleProduct$$"/> + </actionGroup> + + <!-- Assert out of stock option is absent on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <dontSee userInput="$$createConfigProductAttributeOptionThree.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="assertOptionNotAvailable"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml new file mode 100644 index 0000000000000..51f4bf0279942 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml @@ -0,0 +1,111 @@ +<?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="AdminCreateConfigurableProductWithTierPriceForOneItemTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with tier price for one item"/> + <description value="Admin should be able to create configurable product with tier price for one item"/> + <testCaseId value="MC-13695"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create attribute with 2 options to be used in children products --> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionOne"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> + </createData> + + <!--Add tier price in one product --> + <createData entity="tierProductPrice" stepKey="addTierPrice"> + <requiredEntity createDataKey="createFirstSimpleProduct" /> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete created data --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations and add attribute and select all options --> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="generateConfigurationsByAttributeCode" after="fillConfigurableProductValues"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + + <!-- Add associated products to configurations grid --> + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addFirstSimpleProduct"> + <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> + </actionGroup> + + <actionGroup ref="addProductToConfigurationsGrid" stepKey="addSecondSimpleProduct"> + <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> + </actionGroup> + + <!-- Save configurable product --> + <actionGroup ref="saveProductForm" stepKey="saveConfigurableProduct"/> + + <!-- Assert product tier price on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <selectOption userInput="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierProductPrice.quantity}} for ${{tierProductPrice.price}} each and save 27%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml new file mode 100644 index 0000000000000..1db9b3e5b79b2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml @@ -0,0 +1,154 @@ +<?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="AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with two new options assigned to category with not visible child products"/> + <description value="Admin should be able to create configurable product with two new options, assigned to category, child products are not visible individually"/> + <testCaseId value="MC-13685"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete children products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> + </actionGroup> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> + <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two options --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change product configurations in grid --> + <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Add configurable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> + + <!-- Save configurable product; add product to new attribute set --> + <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> + + <!-- Assert child products in grid --> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewFirstChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute1"/> + </actionGroup> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewSecondChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Assert configurable product in grid --> + <actionGroup ref="filterProductGridBySkuAndName" stepKey="findCreatedConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Assert configurable product in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + </actionGroup> + + <!--Assert configurable product on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage" after="assertConfigurableProductInCategory"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Add configurable product to the cart with selected first option --> + <selectOption userInput="{{colorConfigurableProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionForAddingToCart"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> + + <!-- Assert configurable product in cart --> + <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="storefrontCheckCartConfigurableProductActionGroup"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + + <!-- Assert child products are not displayed separately: two next step --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + + <!-- Quick search the storefront for the first attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> + + <!-- Quick search the storefront for the second attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute2.sku}}]" stepKey="searchStorefrontSecondChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute2.name)}}" stepKey="dontSeeConfigurableProductSecondChild"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml new file mode 100644 index 0000000000000..934a410d58a8a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml @@ -0,0 +1,136 @@ +<?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="AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with two new options without assigned to category with not visible child products"/> + <description value="Admin should be able to create configurable product with two options without assigned to category, child products are not visible individually"/> + <testCaseId value="MC-13686"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete children products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> + </actionGroup> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> + <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Create product configurations--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two option --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change product configurations in grid --> + <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Save configurable product; add product to new attribute set --> + <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> + + <!-- Assert Child Products in grid --> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewFirstChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute1"/> + </actionGroup> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewSecondChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Assert Configurable Product in grid --> + <actionGroup ref="filterProductGridBySkuAndName" stepKey="findCreatedConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Assert configurable product on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Add configurable product to the cart with selected first option --> + <selectOption userInput="{{colorConfigurableProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionForAddingToCart"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> + + <!-- Assert configurable product in cart --> + <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="storefrontCheckCartConfigurableProductActionGroup"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + + <!-- Assert child products are not displayed separately: two next step --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + + <!-- Quick search the storefront for the first attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> + + <!-- Quick search the storefront for the second attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute2.sku}}]" stepKey="searchStorefrontSecondChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute2.name)}}" stepKey="dontSeeConfigurableProductSecondChild"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml new file mode 100644 index 0000000000000..fb2920be528b6 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Delete products"/> + <title value="Delete configurable product test"/> + <description value="Admin should be able to delete a configurable product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11020"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="BaseConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProductFilteredBySkuAndName"> + <argument name="product" value="$$createConfigurableProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigurableProduct.name$$)}}" stepKey="amOnConfigurableProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigurableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createConfigurableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createConfigurableProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml index 454f9f5f29a7a..c303e4d19db81 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchConfigurableByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="ConfigurableProduct"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml new file mode 100644 index 0000000000000..456be43f80b8d --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -0,0 +1,99 @@ +<?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="ProductsQtyReturnAfterOrderCancel"> + + <annotations> + <features value="ConfigurableProduct"/> + <title value="Product qunatity return after order cancel"/> + <description value="Check Product qunatity 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="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> + </after> + + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage1"/> + + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="changeProductQuantity"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveChanges"/> + <waitForPageLoad stepKey="waitProductGridToBeLoaded"/> + + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <fillField selector="{{StorefrontProductInfoMainSection.qty}}" userInput="4" stepKey="fillQuantity"/> + + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <waitForPageLoad stepKey="waitForNewInvoicePageLoad"/> + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="1" stepKey="ChangeQtyToInvoice"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQunatity"/> + <waitForPageLoad stepKey="waitPageToBeLoaded"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <waitForPageLoad stepKey="waitOrderDetailToLoad"/> + <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="1" stepKey="changeItemQtyToShip"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <waitForPageLoad stepKey="waitShipmentSectionToLoad"/> + <actionGroup ref="cancelPendingOrder" stepKey="cancelPendingOption"> + <argument name="orderStatus" value="Complete"/> + </actionGroup> + + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQuantity"/> + + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> + + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createConfigProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Quantity')}}" userInput="99" stepKey="seeProductSkuInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index 72fce95ade68d..57c45ee1e8997 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -153,9 +153,9 @@ <argument name="sortBy" value="price"/> <argument name="sort" value="desc"/> </actionGroup> - <see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/> - <see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> - <see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> <!-- Delete the rule --> <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml new file mode 100644 index 0000000000000..bb69122dc0be9 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -0,0 +1,149 @@ +<?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="StorefrontVerifyConfigurableProductLayeredNavigationTest"> + <annotations> + <stories value="Create configurable product"/> + <title value="Out of stock configurable attribute option doesn't show in Layered Navigation"/> + <description value=" Login as admin and verify out of stock configurable attribute option doesn't show in Layered Navigation"/> + <testCaseId value="MC-13734"/> + <severity value="CRITICAL"/> + <group value="ConfigurableProduct"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + + <!-- Create Default Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create an attribute with three options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the third option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!--Create a simple product and give it the attribute with the Third option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Add the third simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + </before> + <after> + <!-- Delete Created Data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product Index Page and Filter First Child product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="ApiSimpleOne"/> + </actionGroup> + + <!-- Change the First Child Product stock status as 'Out Of Stock'--> + <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"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Open Category in Store Front and select product attribute option from sidebar --> + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="$$createConfigProductAttribute.default_value$$"/> + </actionGroup> + + <!--Assert Out Of Stock product is not visible in Storefront Page --> + <dontSee selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="dontSeeOption1"/> + <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="seeOption2"/> + <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="seeOption3"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 25d8412c91056..c5c2368720b98 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -379,6 +379,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): 'percentage' => $percentage, ], ], + 'msrpPrice' => [ + 'amount' => null , + ] ], ], 'priceFormat' => [], diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php new file mode 100644 index 0000000000000..b4fb5ccfaa558 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin\Frontend; + +use Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class ProductIdentitiesExtenderTest + */ +class ProductIdentitiesExtenderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Configurable + */ + private $configurableTypeMock; + + /** + * @var ProductIdentitiesExtender + */ + private $plugin; + + /** @var MockObject|\Magento\Catalog\Model\Product */ + private $product; + + protected function setUp() + { + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMock(); + + $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock); + } + + public function testAfterGetIdentities() + { + $identities = [ + 'SomeCacheId', + 'AnotherCacheId', + ]; + $productId = 12345; + $childIdentities = [ + 0 => [1, 2, 5, 100500] + ]; + $expectedIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + Product::CACHE_TAG . '_' . 1, + Product::CACHE_TAG . '_' . 2, + Product::CACHE_TAG . '_' . 5, + Product::CACHE_TAG . '_' . 100500, + ]; + + $this->product->expects($this->once()) + ->method('getId') + ->willReturn($productId); + + $this->configurableTypeMock->expects($this->once()) + ->method('getChildrenIds') + ->with($productId) + ->willReturn($childIdentities); + + $productIdentities = $this->plugin->afterGetIdentities($this->product, $identities); + $this->assertEquals($expectedIdentities, $productIdentities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php index 64b9b3776442a..ab52d4eb86021 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php @@ -6,22 +6,47 @@ namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\Option; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price as ConfigurablePrice; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\PriceInfo\Base as PriceInfoBase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; class PriceTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price */ + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var ConfigurablePrice + */ protected $model; - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; + /** + * @var ManagerInterface|MockObject + */ + private $eventManagerMock; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->eventManagerMock = $this->createPartialMock( + ManagerInterface::class, + ['dispatch'] + ); $this->model = $this->objectManagerHelper->getObject( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price::class + ConfigurablePrice::class, + ['eventManager' => $this->eventManagerMock] ); } @@ -29,29 +54,29 @@ public function testGetFinalPrice() { $finalPrice = 10; $qty = 1; - $configurableProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp']) - ->getMock(); - $customOption = $this->getMockBuilder(\Magento\Catalog\Model\Product\Configuration\Item\Option::class) + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getProduct']) + ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice']) ->getMock(); - $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) + /** @var PriceInfoBase|MockObject $priceInfo */ + $priceInfo = $this->getMockBuilder(PriceInfoBase::class) ->disableOriginalConstructor() ->setMethods(['getPrice']) ->getMock(); - $price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + /** @var PriceInterface|MockObject $price */ + $price = $this->getMockBuilder(PriceInterface::class) ->disableOriginalConstructor() ->getMock(); - $amount = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) + /** @var AmountInterface|MockObject $amount */ + $amount = $this->getMockBuilder(AmountInterface::class) ->disableOriginalConstructor() ->getMock(); $configurableProduct->expects($this->any()) ->method('getCustomOption') ->willReturnMap([['simple_product', false], ['option_ids', false]]); - $customOption->expects($this->never())->method('getProduct'); $configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); $priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price); $price->expects($this->once())->method('getAmount')->willReturn($amount); @@ -60,4 +85,60 @@ public function testGetFinalPrice() $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct)); } + + public function testGetFinalPriceWithSimpleProduct() + { + $finalPrice = 10; + $qty = 1; + $customerGroupId = 1; + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->createPartialMock( + Product::class, + ['getCustomOption', 'setFinalPrice', 'getCustomerGroupId'] + ); + /** @var Option|MockObject $customOption */ + $customOption = $this->createPartialMock( + Option::class, + ['getProduct'] + ); + /** @var Product|MockObject $simpleProduct */ + $simpleProduct = $this->createPartialMock( + Product::class, + ['setCustomerGroupId', 'setFinalPrice', 'getPrice', 'getTierPrice', 'getData', 'getCustomOption'] + ); + + $configurableProduct->method('getCustomOption') + ->willReturnMap([ + ['simple_product', $customOption], + ['option_ids', false] + ]); + $configurableProduct->method('getCustomerGroupId')->willReturn($customerGroupId); + $configurableProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $customOption->method('getProduct')->willReturn($simpleProduct); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setCustomerGroupId') + ->with($customerGroupId) + ->willReturnSelf(); + $simpleProduct->method('getPrice')->willReturn($finalPrice); + $simpleProduct->method('getTierPrice')->with($qty)->willReturn($finalPrice); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $simpleProduct->method('getData')->with('final_price')->willReturn($finalPrice); + $simpleProduct->method('getCustomOption')->with('option_ids')->willReturn(false); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with('catalog_product_get_final_price', ['product' => $simpleProduct, 'qty' => $qty]); + + $this->assertEquals( + $finalPrice, + $this->model->getFinalPrice($qty, $configurableProduct), + 'The final price calculation is wrong' + ); + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php new file mode 100644 index 0000000000000..80979148c4959 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php @@ -0,0 +1,226 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\SalesRule\Model\Rule\Condition; + +use Magento\Backend\Helper\Data; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product as ValidatorPlugin; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\Locale\Format; +use Magento\Framework\Locale\FormatInterface; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Rule\Model\Condition\Context; +use Magento\SalesRule\Model\Rule\Condition\Product as SalesRuleProduct; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.LongVariable) + */ +class ProductTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var SalesRuleProduct + */ + private $validator; + + /** + * @var \Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product + */ + private $validatorPlugin; + + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->validator = $this->createValidator(); + $this->validatorPlugin = $this->objectManager->getObject(ValidatorPlugin::class); + } + + /** + * @return \Magento\SalesRule\Model\Rule\Condition\Product + */ + private function createValidator(): SalesRuleProduct + { + /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ + $contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Data|\PHPUnit_Framework_MockObject_MockObject $backendHelperMock */ + $backendHelperMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Config|\PHPUnit_Framework_MockObject_MockObject $configMock */ + $configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProductFactory|\PHPUnit_Framework_MockObject_MockObject $productFactoryMock */ + $productFactoryMock = $this->getMockBuilder(ProductFactory::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $productRepositoryMock */ + $productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->getMockForAbstractClass(); + $attributeLoaderInterfaceMock = $this->getMockBuilder(AbstractEntity::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributesByCode']) + ->getMock(); + $attributeLoaderInterfaceMock + ->expects($this->any()) + ->method('getAttributesByCode') + ->willReturn([]); + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $productMock */ + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['loadAllAttributes', 'getConnection', 'getTable']) + ->getMock(); + $productMock->expects($this->any()) + ->method('loadAllAttributes') + ->willReturn($attributeLoaderInterfaceMock); + /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ + $collectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var FormatInterface|\PHPUnit_Framework_MockObject_MockObject $formatMock */ + $formatMock = new Format( + $this->getMockBuilder(ScopeResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(ResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(CurrencyFactory::class)->disableOriginalConstructor()->getMock() + ); + + return new SalesRuleProduct( + $contextMock, + $backendHelperMock, + $configMock, + $productFactoryMock, + $productRepositoryMock, + $productMock, + $collectionMock, + $formatMock + ); + } + + public function testChildIsUsedForValidation() + { + $configurableProductMock = $this->createProductMock(); + $configurableProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Configurable::TYPE_CODE); + $configurableProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(false); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct', 'getChildren']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($configurableProductMock); + + $simpleProductMock = $this->createProductMock(); + $simpleProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $simpleProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(true); + + $childItem = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMockForAbstractClass(); + $childItem->expects($this->any()) + ->method('getProduct') + ->willReturn($simpleProductMock); + + $item->expects($this->any()) + ->method('getChildren') + ->willReturn([$childItem]); + $item->expects($this->once()) + ->method('setProduct') + ->with($simpleProductMock); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } + + /** + * @return Product|\PHPUnit_Framework_MockObject_MockObject + */ + private function createProductMock(): \PHPUnit_Framework_MockObject_MockObject + { + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getAttribute', + 'getId', + 'setQuoteItemQty', + 'setQuoteItemPrice', + 'getTypeId', + 'hasData', + ]) + ->getMock(); + $productMock + ->expects($this->any()) + ->method('setQuoteItemQty') + ->willReturnSelf(); + $productMock + ->expects($this->any()) + ->method('setQuoteItemPrice') + ->willReturnSelf(); + + return $productMock; + } + + public function testChildIsNotUsedForValidation() + { + $simpleProductMock = $this->createProductMock(); + $simpleProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $simpleProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(true); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($simpleProductMock); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php new file mode 100644 index 0000000000000..1a5c6c0003bfa --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector as CommonTaxCollectorPlugin; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Test for CommonTaxCollector plugin + */ +class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var CommonTaxCollectorPlugin + */ + private $commonTaxCollectorPlugin; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->commonTaxCollectorPlugin = $this->objectManager->getObject(CommonTaxCollectorPlugin::class); + } + + /** + * Test to apply Tax Class Id from child item for configurable product + */ + public function testAfterMapItem() + { + $childTaxClassId = 10; + + /** @var Product|MockObject $childProductMock */ + $childProductMock = $this->createPartialMock( + Product::class, + ['getTaxClassId'] + ); + $childProductMock->method('getTaxClassId')->willReturn($childTaxClassId); + /* @var AbstractItem|MockObject $quoteItemMock */ + $childQuoteItemMock = $this->createMock( + AbstractItem::class + ); + $childQuoteItemMock->method('getProduct')->willReturn($childProductMock); + + /** @var Product|MockObject $productMock */ + $productMock = $this->createPartialMock( + Product::class, + ['getTypeId'] + ); + $productMock->method('getTypeId')->willReturn(Configurable::TYPE_CODE); + /* @var AbstractItem|MockObject $quoteItemMock */ + $quoteItemMock = $this->createPartialMock( + AbstractItem::class, + ['getProduct', 'getHasChildren', 'getChildren', 'getQuote', 'getAddress', 'getOptionByCode'] + ); + $quoteItemMock->method('getProduct')->willReturn($productMock); + $quoteItemMock->method('getHasChildren')->willReturn(true); + $quoteItemMock->method('getChildren')->willReturn([$childQuoteItemMock]); + + /* @var TaxClassKeyInterface|MockObject $taxClassObjectMock */ + $taxClassObjectMock = $this->createMock(TaxClassKeyInterface::class); + $taxClassObjectMock->expects($this->once())->method('setValue')->with($childTaxClassId); + + /* @var QuoteDetailsItemInterface|MockObject $quoteDetailsItemMock */ + $quoteDetailsItemMock = $this->createMock(QuoteDetailsItemInterface::class); + $quoteDetailsItemMock->method('getTaxClassKey')->willReturn($taxClassObjectMock); + + $this->commonTaxCollectorPlugin->afterMapItem( + $this->createMock(CommonTaxCollector::class), + $quoteDetailsItemMock, + $this->createMock(QuoteDetailsItemInterfaceFactory::class), + $quoteItemMock + ); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php index fbab25ff1bea6..e0cc83922e03e 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php @@ -5,14 +5,14 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Model\Locator\LocatorInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Sku; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; +use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; -use Magento\Ui\Component\Form; use Magento\Ui\Component\DynamicRows; +use Magento\Ui\Component\Form; use Magento\Ui\Component\Modal; -use Magento\Framework\UrlInterface; -use Magento\Catalog\Model\Locator\LocatorInterface; /** * Data provider for Configurable panel @@ -90,7 +90,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -98,7 +98,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -197,7 +197,7 @@ public function modifyMeta(array $meta) 'autoRender' => false, 'componentType' => 'insertListing', 'component' => 'Magento_ConfigurableProduct/js' - .'/components/associated-product-insert-listing', + . '/components/associated-product-insert-listing', 'dataScope' => $this->associatedListingPrefix . static::ASSOCIATED_PRODUCT_LISTING, 'externalProvider' => $this->associatedListingPrefix @@ -328,14 +328,12 @@ protected function getButtonSet() 'component' => 'Magento_Ui/js/form/components/button', 'actions' => [ [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'trigger', 'params' => ['active', true], ], [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'openModal', ], ], @@ -471,8 +469,7 @@ protected function getRows() 'sku', __('SKU'), [ - 'validation' => - [ + 'validation' => [ 'required-entry' => true, 'max_text_length' => Sku::SKU_MAX_LENGTH, ], @@ -577,6 +574,7 @@ protected function getColumn( 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => $name, 'visibleIfCanEdit' => false, + 'labelVisible' => false, 'imports' => [ 'visible' => '!${$.provider}:${$.parentScope}.canEdit' ], @@ -595,6 +593,7 @@ protected function getColumn( 'component' => 'Magento_Ui/js/form/components/group', 'label' => $label, 'dataScope' => '', + 'showLabel' => false ]; $container['children'] = [ $name . '_edit' => $fieldEdit, diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index e795ea7cd3618..76096fd6bdf67 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -25,7 +25,8 @@ "magento/module-sales-rule": "*", "magento/module-product-video": "*", "magento/module-configurable-sample-data": "*", - "magento/module-product-links-sample-data": "*" + "magento/module-product-links-sample-data": "*", + "magento/module-tax": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 102ed1314f864..c3ffe988b00d7 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -242,4 +242,17 @@ </argument> </arguments> </type> + <type name="Magento\SalesRule\Model\Rule\Condition\Product"> + <plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" /> + </type> + <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> + <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> + </type> + <type name="Magento\Eav\Model\Entity\Attribute\Group"> + <arguments> + <argument name="reservedSystemNames" xsi:type="array"> + <item name="configurable" xsi:type="string">configurable</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index bb830c36b929d..df96829b354c8 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -10,4 +10,7 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> </type> + <type name="Magento\Catalog\Model\Product"> + <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" /> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index a8712cdc183de..ecc95cbe3d48f 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,12 +17,11 @@ <legend class="legend admin__legend"> <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> </legend> - <div class="product-options"> - <div class="field admin__field _required required"> - <?php foreach ($_attributes as $_attribute): ?> - <label class="label admin__field-label"><?php - /* @escapeNotVerified */ echo $_attribute->getProductAttribute() - ->getStoreLabel($_product->getStoreId()); + <div class="product-options fieldset admin__fieldset"> + <?php foreach ($_attributes as $_attribute): ?> + <div class="field admin__field _required required"> + <label class="label admin__field-label"><?= + $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())); ?></label> <div class="control admin__field-control <?php if ($_attribute->getDecoratedIsLast()): @@ -34,8 +33,8 @@ <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> </select> </div> - <?php endforeach; ?> - </div> + </div> + <?php endforeach; ?> </div> </fieldset> <script> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js index 94a24779450ae..7b04bebd4d73a 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js @@ -6,8 +6,9 @@ define([ 'underscore', 'uiRegistry', - 'Magento_Ui/js/dynamic-rows/dynamic-rows' -], function (_, registry, dynamicRows) { + 'Magento_Ui/js/dynamic-rows/dynamic-rows', + 'jquery' +], function (_, registry, dynamicRows, $) { 'use strict'; return dynamicRows.extend({ @@ -217,6 +218,8 @@ define([ _.each(tmpData, function (row, index) { path = this.dataScope + '.' + this.index + '.' + (this.startIndex + index); + row.attributes = $('<i></i>').text(row.attributes).html(); + row.sku = $('<i></i>').text(row.sku).html(); this.source.set(path, row); }, this); @@ -401,8 +404,8 @@ define([ product = { 'id': row.productId, 'product_link': row.productUrl, - 'name': row.name, - 'sku': row.sku, + 'name': $('<i></i>').text(row.name).html(), + 'sku': $('<i></i>').text(row.sku).html(), 'status': row.status, 'price': row.price, 'price_currency': row.priceCurrency, diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js index 28e775b984b05..b2ef35546eea8 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js @@ -11,9 +11,6 @@ define([ return Abstract.extend({ defaults: { - listens: { - isConfigurable: 'handlePriceValue' - }, imports: { isConfigurable: '!ns = ${ $.ns }, index = configurable-matrix:isEmpty' }, @@ -22,12 +19,15 @@ define([ } }, - /** - * Invokes initialize method of parent class, - * contains initialization logic - */ + /** @inheritdoc */ initialize: function () { this._super(); + // resolve initial disable state + this.handlePriceValue(this.isConfigurable); + // add listener to track "configurable" type + this.setListeners({ + isConfigurable: 'handlePriceValue' + }); return this; }, @@ -50,8 +50,9 @@ define([ * @param {String} isConfigurable */ handlePriceValue: function (isConfigurable) { + this.disabled(!!this.isUseDefault() || isConfigurable); + if (isConfigurable) { - this.disable(); this.clear(); } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js index 8d27e3dc58a4a..6e82fd42692fc 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js @@ -383,12 +383,19 @@ define([ * Chose action for the form save button */ saveFormHandler: function () { - this.serializeData(); + this.formElement().validate(); + + if (this.formElement().source.get('params.invalid') === false) { + this.serializeData(); + } if (this.checkForNewAttributes()) { this.formSaveParams = arguments; this.attributeSetHandlerModal().openModal(); } else { + if (this.validateForm(this.formElement())) { + this.clearOutdatedData(); + } this.formElement().save(arguments[0], arguments[1]); if (this.formElement().source.get('params.invalid')) { @@ -397,6 +404,17 @@ define([ } }, + /** + * @param {Object} formElement + * + * Validates each form element and returns true, if all elements are valid. + */ + validateForm: function (formElement) { + formElement.validate(); + + return !formElement.additionalInvalid && !formElement.source.get('params.invalid'); + }, + /** * Serialize data for specific form fields * @@ -414,12 +432,27 @@ define([ if (this.source.data['configurable-matrix']) { this.source.data['configurable-matrix-serialized'] = JSON.stringify(this.source.data['configurable-matrix']); - delete this.source.data['configurable-matrix']; } if (this.source.data['associated_product_ids']) { this.source.data['associated_product_ids_serialized'] = JSON.stringify(this.source.data['associated_product_ids']); + } + }, + + /** + * Clear outdated data for specific form fields + * + * Outdated fields: + * - configurable-matrix; + * - associated_product_ids. + */ + clearOutdatedData: function () { + if (this.source.data['configurable-matrix']) { + delete this.source.data['configurable-matrix']; + } + + if (this.source.data['associated_product_ids']) { delete this.source.data['associated_product_ids']; } }, diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index 6357bbd6c7c0c..e732960421541 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -376,7 +376,8 @@ define([ basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount), optionFinalPrice, optionPriceDiff, - optionPrices = this.options.spConfig.optionPrices; + optionPrices = this.options.spConfig.optionPrices, + allowedProductMinPrice; this._clearSelect(element); element.options[0] = new Option('', ''); @@ -407,8 +408,8 @@ define([ if (typeof allowedProducts[0] !== 'undefined' && typeof optionPrices[allowedProducts[0]] !== 'undefined') { - - optionFinalPrice = parseFloat(optionPrices[allowedProducts[0]].finalPrice.amount); + allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); + optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); optionPriceDiff = optionFinalPrice - basePrice; if (optionPriceDiff !== 0) { @@ -489,36 +490,27 @@ define([ _getPrices: function () { var prices = {}, elements = _.toArray(this.options.settings), - hasProductPrice = false, - optionPriceDiff = 0, - allowedProduct, optionPrices, basePrice, optionFinalPrice; + allowedProduct; _.each(elements, function (element) { var selected = element.options[element.selectedIndex], config = selected && selected.config, priceValue = {}; - if (config && config.allowedProducts.length === 1 && !hasProductPrice) { - prices = {}; + if (config && config.allowedProducts.length === 1) { priceValue = this._calculatePrice(config); - hasProductPrice = true; } else if (element.value) { allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); - optionPrices = this.options.spConfig.optionPrices; - basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount); - - if (!_.isEmpty(allowedProduct)) { - optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - optionPriceDiff = optionFinalPrice - basePrice; - } - - if (optionPriceDiff !== 0) { - prices = {}; - priceValue = this._calculatePriceDifference(allowedProduct); - } + priceValue = this._calculatePrice({ + 'allowedProducts': [ + allowedProduct + ] + }); } - prices[element.attributeId] = priceValue; + if (!_.isEmpty(priceValue)) { + prices.prices = priceValue; + } }, this); return prices; @@ -539,40 +531,15 @@ define([ _.each(allowedProducts, function (allowedProduct) { optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - if (_.isEmpty(product)) { + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { optionMinPrice = optionFinalPrice; product = allowedProduct; } - - if (optionFinalPrice < optionMinPrice) { - product = allowedProduct; - } }, this); return product; }, - /** - * Calculate price difference for allowed product - * - * @param {*} allowedProduct - Product - * @returns {*} - * @private - */ - _calculatePriceDifference: function (allowedProduct) { - var displayPrices = $(this.options.priceHolderSelector).priceBox('option').prices, - newPrices = this.options.spConfig.optionPrices[allowedProduct]; - - _.each(displayPrices, function (price, code) { - - if (newPrices[code]) { - displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount; - } - }); - - return displayPrices; - }, - /** * Returns prices for configured products * @@ -642,6 +609,13 @@ define([ } else { $(this.options.slyOldPriceSelector).hide(); } + + $(document).trigger('updateMsrpPriceBlock', + [ + optionId, + this.options.spConfig.optionPrices + ] + ); }, /** diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php b/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php index aae39800cdd30..eda2ce11daaf6 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php @@ -8,19 +8,25 @@ namespace Magento\ConfigurableProductGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as Type; /** - * {@inheritdoc} + * @inheritdoc */ class ConfigurableProductTypeResolver implements TypeResolverInterface { /** - * {@inheritdoc} + * Configurable product type resolver code */ - public function resolveType(array $data) : string + const TYPE_RESOLVER = 'ConfigurableProduct'; + + /** + * @inheritdoc + */ + public function resolveType(array $data): string { - if (isset($data['type_id']) && $data['type_id'] == 'configurable') { - return 'ConfigurableProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { + return self::TYPE_RESOLVER; } return ''; } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index 90ed5cf54892d..36ee00d55339b 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -124,6 +124,8 @@ private function fetch() : array $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] = $attribute->getProductAttribute()->getAttributeCode(); $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; + $this->attributeMap[$productId][$attribute->getId()]['label'] + = $attribute->getProductAttribute()->getStoreLabel(); } return $this->attributeMap; diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php index a6e39f693b0e5..3e07fecb2ebe7 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php @@ -71,9 +71,7 @@ public function __construct( } /** - * Fetch and format configurable variants. - * - * {@inheritDoc} + * @inheritdoc */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { @@ -85,7 +83,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return $this->valueFactory->create($result); } - $this->variantCollection->addParentId((int)$value[$linkField]); + $this->variantCollection->addParentProduct($value['model']); $fields = $this->getProductFields($info); $matchedFields = $this->attributeCollection->getRequestAttributes($fields); $this->variantCollection->addEavAttributes($matchedFields); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index 658e898f09f81..dd2b84e1da539 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -7,6 +7,9 @@ namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Variant; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -17,9 +20,17 @@ class Attributes implements ResolverInterface { /** + * @inheritdoc + * * Format product's option data to conform to GraphQL schema * - * {@inheritdoc} + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return mixed|Value */ public function resolve( Field $field, @@ -35,12 +46,14 @@ public function resolve( $data = []; foreach ($value['options'] as $option) { $code = $option['attribute_code']; - if (!isset($value['product'][$code])) { + /** @var Product|null $model */ + $model = $value['product']['model'] ?? null; + if (!$model || !$model->getData($code)) { continue; } foreach ($option['values'] as $optionValue) { - if ($optionValue['value_index'] != $value['product'][$code]) { + if ($optionValue['value_index'] != $model->getData($code)) { continue; } $data[] = [ diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 0d86e16574395..12571602878d1 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -9,9 +9,9 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ChildCollection; use Magento\Catalog\Model\ProductFactory; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ChildCollection; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as DataProvider; @@ -47,9 +47,9 @@ class Collection private $metadataPool; /** - * @var int[] + * @var Product[] */ - private $parentIds = []; + private $parentProducts = []; /** * @var array @@ -83,19 +83,24 @@ public function __construct( } /** - * Add parent Id to collection filter + * Add parent to collection filter * - * @param int $id + * @param Product $product * @return void */ - public function addParentId(int $id) : void + public function addParentProduct(Product $product) : void { - if (!in_array($id, $this->parentIds) && !empty($this->childrenMap)) { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $productId = $product->getData($linkField); + + if (isset($this->parentProducts[$productId])) { + return; + } + + if (!empty($this->childrenMap)) { $this->childrenMap = []; - $this->parentIds[] = $id; - } elseif (!in_array($id, $this->parentIds)) { - $this->parentIds[] = $id; } + $this->parentProducts[$productId] = $product; } /** @@ -130,21 +135,20 @@ public function getChildProductsByParentId(int $id) : array * Fetch all children products from parent id's. * * @return array + * @throws \Exception */ private function fetch() : array { - if (empty($this->parentIds) || !empty($this->childrenMap)) { + if (empty($this->parentProducts) || !empty($this->childrenMap)) { return $this->childrenMap; } - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - foreach ($this->parentIds as $id) { + foreach ($this->parentProducts as $product) { + $attributeData = $this->getAttributesCodes($product); /** @var ChildCollection $childCollection */ $childCollection = $this->childCollectionFactory->create(); - /** @var Product $product */ - $product = $this->productFactory->create(); - $product->setData($linkField, $id); $childCollection->setProductFilter($product); + $childCollection->addAttributeToSelect($attributeData); /** @var Product $childProduct */ foreach ($childCollection->getItems() as $childProduct) { @@ -160,4 +164,24 @@ private function fetch() : array return $this->childrenMap; } + + /** + * Get attributes code + * + * @param \Magento\Catalog\Model\Product $currentProduct + * @return array + */ + private function getAttributesCodes(Product $currentProduct): array + { + $attributeCodes = []; + $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); + foreach ($allowAttributes as $attribute) { + $productAttribute = $attribute->getProductAttribute(); + if (!\in_array($productAttribute->getAttributeCode(), $attributeCodes)) { + $attributeCodes[] = $productAttribute->getAttributeCode(); + } + } + + return $attributeCodes; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 267a94a1d434e..d4780c5c0867a 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -1,5 +1,8 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type Mutation { + addConfigurableProductsToCart(input: AddConfigurableProductsToCartInput): AddConfigurableProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") +} type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") @@ -35,3 +38,30 @@ type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOpti store_label: String @doc(description: "The label of the product on the current store") use_default_value: Boolean @doc(description: "Indicates whether to use the default_label") } + +input AddConfigurableProductsToCartInput { + cart_id: String! + cartItems: [ConfigurableProductCartItemInput!]! +} + +type AddConfigurableProductsToCartOutput { + cart: Cart! +} + +input ConfigurableProductCartItemInput { + data: CartItemInput! + variant_sku: String! + customizable_options:[CustomizableOptionInput!] +} + +type ConfigurableCartItem implements CartItemInterface { + customizable_options: [SelectedCustomizableOption]! + configurable_options: [SelectedConfigurableOption!]! +} + +type SelectedConfigurableOption { + id: Int! + option_label: String! + value_id: Int! + value_label: String! +} diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml new file mode 100644 index 0000000000000..eec2194825166 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.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="AssertMessageContactUsFormActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Thanks for contacting us with your comments and questions. We'll respond to you very soon." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontContactUsMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml new file mode 100644 index 0000000000000..df4964ea0423d --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.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="StorefrontFillContactUsFormActionGroup"> + <arguments> + <argument name="customer" type="entity" /> + <argument name="contactUsData" type="entity" /> + </arguments> + <fillField selector="{{StorefrontContactUsFormSection.nameField}}" userInput="{{customer.firstname}}" stepKey="fillName"/> + <fillField selector="{{StorefrontContactUsFormSection.emailField}}" userInput="{{customer.email}}" stepKey="fillEmail"/> + <fillField selector="{{StorefrontContactUsFormSection.commentField}}" userInput="{{contactUsData.comment}}" stepKey="fillComment"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml new file mode 100644 index 0000000000000..d333d5d998960 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontOpenContactUsPageActionGroup"> + <amOnPage url="{{StorefrontContactUsPage.url}}" stepKey="amOnContactUpPage"/> + <waitForPageLoad stepKey="waitForContactUpPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml new file mode 100644 index 0000000000000..f3fe34f20c319 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontSubmitContactUsFormActionGroup"> + <click selector="{{StorefrontContactUsFormSection.submitFormButton}}" stepKey="clickSubmitFormButton"/> + <waitForPageLoad stepKey="waitForCommentSubmitted" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml b/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml new file mode 100644 index 0000000000000..eadf760776c58 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml @@ -0,0 +1,14 @@ +<?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="DefaultContactUsData"> + <data key="comment" unique="suffix">Lorem ipsum dolor sit amet, ne enim aliquando eam, oblique deserunt no usu. Unique: </data> + </entity> +</entities> diff --git a/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml b/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml new file mode 100644 index 0000000000000..5e793b2338507 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontContactUsPage" url="/contact/" area="storefront" module="Magento_Contact"> + <section name="StorefrontContactUsFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml new file mode 100644 index 0000000000000..fdaddf33f5170 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsFormSection"> + <element name="nameField" type="input" selector="#contact-form input[name='name']" /> + <element name="emailField" type="input" selector="#contact-form input[name='email']" /> + <element name="phoneField" type="input" selector="#contact-form input[name='telephone']" /> + <element name="commentField" type="textarea" selector="#contact-form textarea[name='comment']" /> + <element name="submitFormButton" type="button" selector="#contact-form button[type='submit']" timeout="30" /> + </section> +</sections> diff --git a/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml new file mode 100644 index 0000000000000..0970f1f8f6b20 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.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="StorefrontContactUsMessagesSection"> + <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less index 0aaec05aa2afe..d79806eecbe9b 100644 --- a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less +++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less @@ -21,6 +21,16 @@ } } +// +// Desktop +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { + .contact-index-index .column:not(.sidebar-additional) .form.contact { + min-width: 600px; + } +} + // Mobile .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .contact-index-index { diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php index 05ab02d7a2a1a..8bab596ab4c13 100644 --- a/app/code/Magento/Cookie/Helper/Cookie.php +++ b/app/code/Magento/Cookie/Helper/Cookie.php @@ -42,7 +42,8 @@ class Cookie extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $data * - * @throws \InvalidArgumentException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function __construct( \Magento\Framework\App\Helper\Context $context, diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index 200b0fd690882..582c7c811b71f 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\CronException; use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Intl\DateTimeFactory; /** * Crontab schedule model @@ -50,13 +51,19 @@ class Schedule extends \Magento\Framework\Model\AbstractModel */ private $timezoneConverter; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param TimezoneInterface $timezoneConverter + * @param TimezoneInterface|null $timezoneConverter + * @param DateTimeFactory|null $dateTimeFactory */ public function __construct( \Magento\Framework\Model\Context $context, @@ -64,10 +71,12 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - TimezoneInterface $timezoneConverter = null + TimezoneInterface $timezoneConverter = null, + DateTimeFactory $dateTimeFactory = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); } /** @@ -111,17 +120,20 @@ public function trySchedule() if (!$e || !$time) { return false; } + $configTimeZone = $this->timezoneConverter->getConfigTimezone(); + $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); if (!is_numeric($time)) { //convert time from UTC to admin store timezone //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone - $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); - $time = strtotime($time); + $dateTimeUtc = $this->dateTimeFactory->create($time); + $time = $dateTimeUtc->getTimestamp(); } - $match = $this->matchCronExpression($e[0], strftime('%M', $time)) - && $this->matchCronExpression($e[1], strftime('%H', $time)) - && $this->matchCronExpression($e[2], strftime('%d', $time)) - && $this->matchCronExpression($e[3], strftime('%m', $time)) - && $this->matchCronExpression($e[4], strftime('%w', $time)); + $time = $storeDateTime->setTimestamp($time); + $match = $this->matchCronExpression($e[0], $time->format('i')) + && $this->matchCronExpression($e[1], $time->format('H')) + && $this->matchCronExpression($e[2], $time->format('d')) + && $this->matchCronExpression($e[3], $time->format('m')) + && $this->matchCronExpression($e[4], $time->format('w')); return $match; } diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index e9f4c61c7f551..da5539859a4b5 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php @@ -6,6 +6,9 @@ namespace Magento\Cron\Test\Unit\Model; use Magento\Cron\Model\Schedule; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** * Class \Magento\Cron\Test\Unit\Model\ObserverTest @@ -18,11 +21,27 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase */ protected $helper; + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule + */ protected $resourceJobMock; + /** + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $timezoneConverter; + + /** + * @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeFactory; + + /** + * @inheritdoc + */ protected function setUp() { - $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->helper = new ObjectManager($this); $this->resourceJobMock = $this->getMockBuilder(\Magento\Cron\Model\ResourceModel\Schedule::class) ->disableOriginalConstructor() @@ -32,18 +51,30 @@ protected function setUp() $this->resourceJobMock->expects($this->any()) ->method('getIdFieldName') ->will($this->returnValue('id')); + + $this->timezoneConverter = $this->getMockBuilder(TimezoneInterface::class) + ->setMethods(['date']) + ->getMockForAbstractClass(); + + $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->setMethods(['create']) + ->getMock(); } /** + * Test for SetCronExpr + * * @param string $cronExpression * @param array $expected + * + * @return void * @dataProvider setCronExprDataProvider */ - public function testSetCronExpr($cronExpression, $expected) + public function testSetCronExpr($cronExpression, $expected): void { // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); + /** @var Schedule $model */ + $model = $this->helper->getObject(Schedule::class); // 2. Run tested method $model->setCronExpr($cronExpression); @@ -61,7 +92,7 @@ public function testSetCronExpr($cronExpression, $expected) * * @return array */ - public function setCronExprDataProvider() + public function setCronExprDataProvider(): array { return [ ['1 2 3 4 5', [1, 2, 3, 4, 5]], @@ -121,27 +152,33 @@ public function setCronExprDataProvider() } /** + * Test for SetCronExprException + * * @param string $cronExpression + * + * @return void * @expectedException \Magento\Framework\Exception\CronException * @dataProvider setCronExprExceptionDataProvider */ - public function testSetCronExprException($cronExpression) + public function testSetCronExprException($cronExpression): void { // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); + /** @var Schedule $model */ + $model = $this->helper->getObject(Schedule::class); // 2. Run tested method $model->setCronExpr($cronExpression); } /** + * Data provider + * * Here is a list of allowed characters and values for Cron expression * http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm * * @return array */ - public function setCronExprExceptionDataProvider() + public function setCronExprExceptionDataProvider(): array { return [ [''], @@ -153,17 +190,31 @@ public function setCronExprExceptionDataProvider() } /** + * Test for trySchedule + * * @param int $scheduledAt * @param array $cronExprArr * @param $expected + * + * @return void * @dataProvider tryScheduleDataProvider */ - public function testTrySchedule($scheduledAt, $cronExprArr, $expected) + public function testTrySchedule($scheduledAt, $cronExprArr, $expected): void { // 1. Create mocks + $this->timezoneConverter->method('getConfigTimezone') + ->willReturn('UTC'); + + $this->dateTimeFactory->method('create') + ->willReturn(new \DateTime()); + /** @var \Magento\Cron\Model\Schedule $model */ $model = $this->helper->getObject( - \Magento\Cron\Model\Schedule::class + \Magento\Cron\Model\Schedule::class, + [ + 'timezoneConverter' => $this->timezoneConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + ] ); // 2. Set fixtures @@ -177,22 +228,29 @@ public function testTrySchedule($scheduledAt, $cronExprArr, $expected) $this->assertEquals($expected, $result); } - public function testTryScheduleWithConversionToAdminStoreTime() + /** + * Test for tryScheduleWithConversionToAdminStoreTime + * + * @return void + */ + public function testTryScheduleWithConversionToAdminStoreTime(): void { $scheduledAt = '2011-12-13 14:15:16'; $cronExprArr = ['*', '*', '*', '*', '*']; - // 1. Create mocks - $timezoneConverter = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); - $timezoneConverter->expects($this->once()) - ->method('date') - ->with($scheduledAt) - ->willReturn(new \DateTime($scheduledAt)); + $this->timezoneConverter->method('getConfigTimezone') + ->willReturn('UTC'); + + $this->dateTimeFactory->method('create') + ->willReturn(new \DateTime()); /** @var \Magento\Cron\Model\Schedule $model */ $model = $this->helper->getObject( \Magento\Cron\Model\Schedule::class, - ['timezoneConverter' => $timezoneConverter] + [ + 'timezoneConverter' => $this->timezoneConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + ] ); // 2. Set fixtures @@ -207,11 +265,15 @@ public function testTryScheduleWithConversionToAdminStoreTime() } /** + * Data provider + * * @return array */ - public function tryScheduleDataProvider() + public function tryScheduleDataProvider(): array { $date = '2011-12-13 14:15:16'; + $timestamp = (new \DateTime($date))->getTimestamp(); + $day = 'Monday'; return [ [$date, [], false], [$date, null, false], @@ -219,22 +281,26 @@ public function tryScheduleDataProvider() [$date, [], false], [$date, null, false], [$date, false, false], - [strtotime($date), ['*', '*', '*', '*', '*'], true], - [strtotime($date), ['15', '*', '*', '*', '*'], true], - [strtotime($date), ['*', '14', '*', '*', '*'], true], - [strtotime($date), ['*', '*', '13', '*', '*'], true], - [strtotime($date), ['*', '*', '*', '12', '*'], true], - [strtotime('Monday'), ['*', '*', '*', '*', '1'], true], + [$timestamp, ['*', '*', '*', '*', '*'], true], + [$timestamp, ['15', '*', '*', '*', '*'], true], + [$timestamp, ['*', '14', '*', '*', '*'], true], + [$timestamp, ['*', '*', '13', '*', '*'], true], + [$timestamp, ['*', '*', '*', '12', '*'], true], + [(new \DateTime($day))->getTimestamp(), ['*', '*', '*', '*', '1'], true], ]; } /** + * Test for matchCronExpression + * * @param string $cronExpressionPart * @param int $dateTimePart * @param bool $expectedResult + * + * @return void * @dataProvider matchCronExpressionDataProvider */ - public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult) + public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult): void { // 1. Create mocks /** @var \Magento\Cron\Model\Schedule $model */ @@ -248,9 +314,11 @@ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $exp } /** + * Data provider + * * @return array */ - public function matchCronExpressionDataProvider() + public function matchCronExpressionDataProvider(): array { return [ ['*', 0, true], @@ -287,11 +355,15 @@ public function matchCronExpressionDataProvider() } /** + * Test for matchCronExpressionException + * * @param string $cronExpressionPart + * + * @return void * @expectedException \Magento\Framework\Exception\CronException * @dataProvider matchCronExpressionExceptionDataProvider */ - public function testMatchCronExpressionException($cronExpressionPart) + public function testMatchCronExpressionException($cronExpressionPart): void { $dateTimePart = 10; @@ -304,9 +376,11 @@ public function testMatchCronExpressionException($cronExpressionPart) } /** + * Data provider + * * @return array */ - public function matchCronExpressionExceptionDataProvider() + public function matchCronExpressionExceptionDataProvider(): array { return [ ['1/2/3'], //Invalid cron expression, expecting 'match/modulus': 1/2/3 @@ -317,11 +391,15 @@ public function matchCronExpressionExceptionDataProvider() } /** + * Test for GetNumeric + * * @param mixed $param * @param int $expectedResult + * + * @return void * @dataProvider getNumericDataProvider */ - public function testGetNumeric($param, $expectedResult) + public function testGetNumeric($param, $expectedResult): void { // 1. Create mocks /** @var \Magento\Cron\Model\Schedule $model */ @@ -335,9 +413,11 @@ public function testGetNumeric($param, $expectedResult) } /** + * Data provider + * * @return array */ - public function getNumericDataProvider() + public function getNumericDataProvider(): array { return [ [null, false], @@ -362,7 +442,12 @@ public function getNumericDataProvider() ]; } - public function testTryLockJobSuccess() + /** + * Test for tryLockJobSuccess + * + * @return void + */ + public function testTryLockJobSuccess(): void { $scheduleId = 1; @@ -386,7 +471,12 @@ public function testTryLockJobSuccess() $this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus()); } - public function testTryLockJobFailure() + /** + * Test for tryLockJobFailure + * + * @return void + */ + public function testTryLockJobFailure(): void { $scheduleId = 1; diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index 632a014a0ab77..462dde98f99fc 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -174,7 +174,8 @@ protected function setUp() $this->statFactory = $this->getMockBuilder(StatFactory::class) ->setMethods(['create']) - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMock(); $this->stat = $this->getMockBuilder(\Magento\Framework\Profiler\Driver\Standard\Stat::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..9166c8745c9e1 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?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="AdminMenuStoresCurrencyCurrencyRates"> + <data key="pageTitle">Currency Rates</data> + <data key="title">Currency Rates</data> + <data key="dataUiId">magento-currencysymbol-system-currency-rates</data> + </entity> + <entity name="AdminMenuStoresCurrencyCurrencySymbols"> + <data key="pageTitle">Currency Symbols</data> + <data key="title">Currency Symbols</data> + <data key="dataUiId">magento-currencysymbol-system-currency-symbols</data> + </entity> +</entities> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml new file mode 100644 index 0000000000000..4a33d40d2a35f --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresCurrencyRatesNavigateMenuTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Menu Navigation"/> + <title value="Admin stores currency rates navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Currency Rates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14150"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencyRatesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencyRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresCurrencyCurrencyRates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml new file mode 100644 index 0000000000000..978917772f2dd --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresCurrencySymbolsNavigateMenuTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Menu Navigation"/> + <title value="Admin stores currency symbols navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Currency Symbols"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14151"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencySymbolsPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencySymbols.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresCurrencyCurrencySymbols.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php index e84da5b9fcd57..10fc2349968ea 100644 --- a/app/code/Magento/Customer/Api/AccountManagementInterface.php +++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php @@ -31,15 +31,13 @@ interface AccountManagementInterface * @param \Magento\Customer\Api\Data\CustomerInterface $customer * @param string $password * @param string $redirectUrl - * @param string[] $extensions * @return \Magento\Customer\Api\Data\CustomerInterface * @throws \Magento\Framework\Exception\LocalizedException */ public function createAccount( \Magento\Customer\Api\Data\CustomerInterface $customer, $password = null, - $redirectUrl = '', - $extensions = [] + $redirectUrl = '' ); /** @@ -50,7 +48,6 @@ public function createAccount( * @param string $hash Password hash that we can save directly * @param string $redirectUrl URL fed to welcome email templates. Can be used by templates to, for example, direct * the customer to a product they were looking at after pressing confirmation link. - * @param string[] $extensions * @return \Magento\Customer\Api\Data\CustomerInterface * @throws \Magento\Framework\Exception\InputException If bad input is provided * @throws \Magento\Framework\Exception\State\InputMismatchException If the provided email is already used @@ -59,8 +56,7 @@ public function createAccount( public function createAccountWithPasswordHash( \Magento\Customer\Api\Data\CustomerInterface $customer, $hash, - $redirectUrl = '', - $extensions = [] + $redirectUrl = '' ); /** diff --git a/app/code/Magento/Customer/Block/Address/Book.php b/app/code/Magento/Customer/Block/Address/Book.php index 8b38946a063db..04669446ffee9 100644 --- a/app/code/Magento/Customer/Block/Address/Book.php +++ b/app/code/Magento/Customer/Block/Address/Book.php @@ -6,8 +6,8 @@ namespace Magento\Customer\Block\Address; use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\Address\Mapper; +use Magento\Customer\Block\Address\Grid as AddressesGrid; /** * Customer address book block @@ -24,7 +24,7 @@ class Book extends \Magento\Framework\View\Element\Template protected $currentCustomer; /** - * @var CustomerRepositoryInterface + * @var \Magento\Customer\Api\CustomerRepositoryInterface */ protected $customerRepository; @@ -43,33 +43,44 @@ class Book extends \Magento\Framework\View\Element\Template */ protected $addressMapper; + /** + * @var AddressesGrid + */ + private $addressesGrid; + /** * @param \Magento\Framework\View\Element\Template\Context $context - * @param CustomerRepositoryInterface $customerRepository + * @param CustomerRepositoryInterface|null $customerRepository * @param AddressRepositoryInterface $addressRepository * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer * @param \Magento\Customer\Model\Address\Config $addressConfig * @param Mapper $addressMapper * @param array $data + * @param AddressesGrid|null $addressesGrid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, - CustomerRepositoryInterface $customerRepository, + \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository = null, AddressRepositoryInterface $addressRepository, \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, \Magento\Customer\Model\Address\Config $addressConfig, Mapper $addressMapper, - array $data = [] + array $data = [], + Grid $addressesGrid = null ) { - $this->customerRepository = $customerRepository; $this->currentCustomer = $currentCustomer; $this->addressRepository = $addressRepository; $this->_addressConfig = $addressConfig; $this->addressMapper = $addressMapper; + $this->addressesGrid = $addressesGrid ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(AddressesGrid::class); parent::__construct($context, $data); } /** + * Prepare the Address Book section layout + * * @return $this */ protected function _prepareLayout() @@ -79,14 +90,20 @@ protected function _prepareLayout() } /** + * Generate and return "New Address" URL + * * @return string + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getAddAddressUrl */ public function getAddAddressUrl() { - return $this->getUrl('customer/address/new', ['_secure' => true]); + return $this->addressesGrid->getAddAddressUrl(); } /** + * Generate and return "Back" URL + * * @return string */ public function getBackUrl() @@ -98,24 +115,37 @@ public function getBackUrl() } /** + * Generate and return "Delete" URL + * * @return string + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getDeleteUrl */ public function getDeleteUrl() { - return $this->getUrl('customer/address/delete'); + return $this->addressesGrid->getDeleteUrl(); } /** + * Generate and return "Edit Address" URL. + * + * Address ID passed in parameters + * * @param int $addressId * @return string + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getAddressEditUrl */ public function getAddressEditUrl($addressId) { - return $this->getUrl('customer/address/edit', ['_secure' => true, 'id' => $addressId]); + return $this->addressesGrid->getAddressEditUrl($addressId); } /** + * Determines is the address primary (billing or shipping) + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function hasPrimaryAddress() { @@ -123,22 +153,22 @@ public function hasPrimaryAddress() } /** + * Get current additional customer addresses + * + * Will return array of address interfaces if customer have additional addresses and false in other case. + * * @return \Magento\Customer\Api\Data\AddressInterface[]|bool + * @throws \Magento\Framework\Exception\LocalizedException + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getAdditionalAddresses */ public function getAdditionalAddresses() { try { - $addresses = $this->customerRepository->getById($this->currentCustomer->getCustomerId())->getAddresses(); + $addresses = $this->addressesGrid->getAdditionalAddresses(); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - return false; - } - $primaryAddressIds = [$this->getDefaultBilling(), $this->getDefaultShipping()]; - foreach ($addresses as $address) { - if (!in_array($address->getId(), $primaryAddressIds)) { - $additional[] = $address; - } } - return empty($additional) ? false : $additional; + return empty($addresses) ? false : $addresses; } /** @@ -158,23 +188,23 @@ public function getAddressHtml(\Magento\Customer\Api\Data\AddressInterface $addr } /** + * Get current customer + * * @return \Magento\Customer\Api\Data\CustomerInterface|null */ public function getCustomer() { - $customer = $this->getData('customer'); - if ($customer === null) { - try { - $customer = $this->customerRepository->getById($this->currentCustomer->getCustomerId()); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - return null; - } - $this->setData('customer', $customer); + $customer = null; + try { + $customer = $this->currentCustomer->getCustomer(); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } return $customer; } /** + * Get customer's default billing address + * * @return int|null */ public function getDefaultBilling() @@ -188,8 +218,11 @@ public function getDefaultBilling() } /** + * Get customer address by ID + * * @param int $addressId * @return \Magento\Customer\Api\Data\AddressInterface|null + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAddressById($addressId) { @@ -201,6 +234,8 @@ public function getAddressById($addressId) } /** + * Get customer's default shipping address + * * @return int|null */ public function getDefaultShipping() diff --git a/app/code/Magento/Customer/Block/Address/Edit.php b/app/code/Magento/Customer/Block/Address/Edit.php index 6a42e9670ccc6..afefb1138deac 100644 --- a/app/code/Magento/Customer/Block/Address/Edit.php +++ b/app/code/Magento/Customer/Block/Address/Edit.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Block\Address; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; /** @@ -46,6 +47,11 @@ class Edit extends \Magento\Directory\Block\Data */ protected $dataObjectHelper; + /** + * @var \Magento\Customer\Api\AddressMetadataInterface + */ + private $addressMetadata; + /** * Constructor * @@ -61,6 +67,7 @@ class Edit extends \Magento\Directory\Block\Data * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param array $data + * @param \Magento\Customer\Api\AddressMetadataInterface|null $addressMetadata * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -76,13 +83,15 @@ public function __construct( \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory, \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, - array $data = [] + array $data = [], + \Magento\Customer\Api\AddressMetadataInterface $addressMetadata = null ) { $this->_customerSession = $customerSession; $this->_addressRepository = $addressRepository; $this->addressDataFactory = $addressDataFactory; $this->currentCustomer = $currentCustomer; $this->dataObjectHelper = $dataObjectHelper; + $this->addressMetadata = $addressMetadata; parent::__construct( $context, $directoryHelper, @@ -103,6 +112,32 @@ protected function _prepareLayout() { parent::_prepareLayout(); + $this->initAddressObject(); + + $this->pageConfig->getTitle()->set($this->getTitle()); + + if ($postedData = $this->_customerSession->getAddressFormData(true)) { + $postedData['region'] = [ + 'region_id' => isset($postedData['region_id']) ? $postedData['region_id'] : null, + 'region' => $postedData['region'], + ]; + $this->dataObjectHelper->populateWithArray( + $this->_address, + $postedData, + \Magento\Customer\Api\Data\AddressInterface::class + ); + } + $this->precheckRequiredAttributes(); + return $this; + } + + /** + * Initialize address object. + * + * @return void + */ + private function initAddressObject() + { // Init address object if ($addressId = $this->getRequest()->getParam('id')) { try { @@ -124,22 +159,26 @@ protected function _prepareLayout() $this->_address->setLastname($customer->getLastname()); $this->_address->setSuffix($customer->getSuffix()); } + } - $this->pageConfig->getTitle()->set($this->getTitle()); - - if ($postedData = $this->_customerSession->getAddressFormData(true)) { - $postedData['region'] = [ - 'region_id' => isset($postedData['region_id']) ? $postedData['region_id'] : null, - 'region' => $postedData['region'], - ]; - $this->dataObjectHelper->populateWithArray( - $this->_address, - $postedData, - \Magento\Customer\Api\Data\AddressInterface::class - ); + /** + * Precheck attributes that may be required in attribute configuration. + * + * @return void + */ + private function precheckRequiredAttributes() + { + $precheckAttributes = $this->getData('check_attributes_on_render'); + $requiredAttributesPrechecked = []; + if (!empty($precheckAttributes) && is_array($precheckAttributes)) { + foreach ($precheckAttributes as $attributeCode) { + $attributeMetadata = $this->addressMetadata->getAttributeMetadata($attributeCode); + if ($attributeMetadata && $attributeMetadata->isRequired()) { + $requiredAttributesPrechecked[$attributeCode] = $attributeCode; + } + } } - - return $this; + $this->setData('required_attributes_prechecked', $requiredAttributesPrechecked); } /** diff --git a/app/code/Magento/Customer/Block/Address/Grid.php b/app/code/Magento/Customer/Block/Address/Grid.php new file mode 100644 index 0000000000000..963efc648d94b --- /dev/null +++ b/app/code/Magento/Customer/Block/Address/Grid.php @@ -0,0 +1,250 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Address; + +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; +use Magento\Directory\Model\CountryFactory; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Customer address grid + * + * @api + */ +class Grid extends \Magento\Framework\View\Element\Template +{ + /** + * @var \Magento\Customer\Helper\Session\CurrentCustomer + */ + private $currentCustomer; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address\CollectionFactory + */ + private $addressCollectionFactory; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address\Collection + */ + private $addressCollection; + + /** + * @var CountryFactory + */ + private $countryFactory; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer + * @param AddressCollectionFactory $addressCollectionFactory + * @param CountryFactory $countryFactory + * @param array $data + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, + AddressCollectionFactory $addressCollectionFactory, + CountryFactory $countryFactory, + array $data = [] + ) { + $this->currentCustomer = $currentCustomer; + $this->addressCollectionFactory = $addressCollectionFactory; + $this->countryFactory = $countryFactory; + + parent::__construct($context, $data); + } + + /** + * Prepare the Address Book section layout + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _prepareLayout(): void + { + parent::_prepareLayout(); + $this->preparePager(); + } + + /** + * Generate and return "New Address" URL + * + * @return string + */ + public function getAddAddressUrl(): string + { + return $this->getUrl('customer/address/new', ['_secure' => true]); + } + + /** + * Generate and return "Delete" URL + * + * @return string + */ + public function getDeleteUrl(): string + { + return $this->getUrl('customer/address/delete'); + } + + /** + * Generate and return "Edit Address" URL. + * + * Address ID passed in parameters + * + * @param int $addressId + * @return string + */ + public function getAddressEditUrl($addressId): string + { + return $this->getUrl('customer/address/edit', ['_secure' => true, 'id' => $addressId]); + } + + /** + * Get current additional customer addresses + * + * Return array of address interfaces if customer has additional addresses and false in other cases + * + * @return \Magento\Customer\Api\Data\AddressInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + * @throws NoSuchEntityException + */ + public function getAdditionalAddresses(): array + { + $additional = []; + $addresses = $this->getAddressCollection(); + $primaryAddressIds = [$this->getDefaultBilling(), $this->getDefaultShipping()]; + foreach ($addresses as $address) { + if (!in_array((int)$address->getId(), $primaryAddressIds, true)) { + $additional[] = $address->getDataModel(); + } + } + return $additional; + } + + /** + * Get current customer + * + * Return stored customer or get it from session + * + * @return \Magento\Customer\Api\Data\CustomerInterface + */ + public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface + { + $customer = $this->getData('customer'); + if ($customer === null) { + $customer = $this->currentCustomer->getCustomer(); + $this->setData('customer', $customer); + } + return $customer; + } + + /** + * Get one string street address from the Address DTO passed in parameters + * + * @param \Magento\Customer\Api\Data\AddressInterface $address + * @return string + */ + public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $address): string + { + $street = $address->getStreet(); + if (is_array($street)) { + $street = implode(', ', $street); + } + return $street; + } + + /** + * Get country name by $countryCode + * + * Using \Magento\Directory\Model\Country to get country name by $countryCode + * + * @param string $countryCode + * @return string + */ + public function getCountryByCode(string $countryCode): string + { + /** @var \Magento\Directory\Model\Country $country */ + $country = $this->countryFactory->create(); + return $country->loadByCode($countryCode)->getName(); + } + + /** + * Get default billing address + * + * Return address string if address found and null if not + * + * @return int + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getDefaultBilling(): int + { + $customer = $this->getCustomer(); + + return (int)$customer->getDefaultBilling(); + } + + /** + * Get default shipping address + * + * Return address string if address found and null if not + * + * @return int + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getDefaultShipping(): int + { + $customer = $this->getCustomer(); + + return (int)$customer->getDefaultShipping(); + } + + /** + * Get pager layout + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function preparePager(): void + { + $addressCollection = $this->getAddressCollection(); + if (null !== $addressCollection) { + $pager = $this->getLayout()->createBlock( + \Magento\Theme\Block\Html\Pager::class, + 'customer.addresses.pager' + )->setCollection($addressCollection); + $this->setChild('pager', $pager); + } + } + + /** + * Get customer addresses collection. + * + * Filters collection by customer id + * + * @return \Magento\Customer\Model\ResourceModel\Address\Collection + * @throws NoSuchEntityException + */ + private function getAddressCollection(): \Magento\Customer\Model\ResourceModel\Address\Collection + { + if (null === $this->addressCollection) { + if (null === $this->getCustomer()) { + throw new NoSuchEntityException(__('Customer not logged in')); + } + /** @var \Magento\Customer\Model\ResourceModel\Address\Collection $collection */ + $collection = $this->addressCollectionFactory->create(); + $collection->setOrder('entity_id', 'desc'); + $collection->addFieldToFilter( + 'entity_id', + ['nin' => [$this->getDefaultBilling(), $this->getDefaultShipping()]] + ); + $collection->setCustomerFilter([$this->getCustomer()->getId()]); + $this->addressCollection = $collection; + } + return $this->addressCollection; + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php index e1bb6feb23698..38b2f410d2fab 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php @@ -10,6 +10,7 @@ /** * Class DeleteButton + * * @package Magento\Customer\Block\Adminhtml\Edit */ class DeleteButton extends GenericButton implements ButtonProviderInterface @@ -36,6 +37,8 @@ public function __construct( } /** + * Get button data. + * * @return array */ public function getButtonData() @@ -53,12 +56,15 @@ public function getButtonData() ], 'on_click' => '', 'sort_order' => 20, + 'aclResource' => 'Magento_Customer::delete', ]; } return $data; } /** + * Get delete url. + * * @return string */ public function getDeleteUrl() diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php index 180cb3d66ea35..ca24ac9356df9 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php @@ -9,11 +9,14 @@ /** * Class InvalidateTokenButton + * * @package Magento\Customer\Block\Adminhtml\Edit */ class InvalidateTokenButton extends GenericButton implements ButtonProviderInterface { /** + * Get button data. + * * @return array */ public function getButtonData() @@ -27,12 +30,15 @@ public function getButtonData() 'class' => 'invalidate-token', 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . '")', 'sort_order' => 65, + 'aclResource' => 'Magento_Customer::invalidate_tokens', ]; } return $data; } /** + * Get invalidate token url. + * * @return string */ public function getInvalidateTokenUrl() diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 9a025211c9b0a..0aeed1562c51e 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -48,7 +48,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $regionId = $element->getForm()->getElement('region_id')->getValue(); - $html = '<div class="field field-state required admin__field _required">'; + $html = '<div class="field field-state admin__field">'; $element->setClass('input-text admin__control-text'); $element->setRequired(true); $html .= $element->getLabelHtml() . '<div class="control admin__field-control">'; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php index aa93785116851..f8a6b3505ae68 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php @@ -27,6 +27,7 @@ public function getButtonData() 'class' => 'reset reset-password', 'on_click' => sprintf("location.href = '%s';", $this->getResetPasswordUrl()), 'sort_order' => 60, + 'aclResource' => 'Magento_Customer::reset_password', ]; } return $data; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php index f2b8133e352ad..2fb59ec767e8a 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php @@ -63,7 +63,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_orders_grid'); - $this->setDefaultSort('created_at', 'desc'); + $this->setDefaultSort('created_at'); + $this->setDefaultDir('desc'); $this->setUseAjax(true); } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php index 3f2c7cda7608d..e63c00ba18d29 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php @@ -71,13 +71,14 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _construct() { parent::_construct(); $this->setId('customer_view_cart_grid'); - $this->setDefaultSort('added_at', 'desc'); + $this->setDefaultSort('added_at'); + $this->setDefaultDir('desc'); $this->setSortable(false); $this->setPagerVisibility(false); $this->setFilterVisibility(false); @@ -94,7 +95,7 @@ protected function _prepareCollection() $quote = $this->getQuote(); if ($quote) { - $collection = $quote->getItemsCollection(false); + $collection = $quote->getItemsCollection(true); } else { $collection = $this->_dataCollectionFactory->create(); } @@ -106,7 +107,7 @@ protected function _prepareCollection() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -144,7 +145,7 @@ protected function _prepareColumns() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRowUrl($row) { @@ -152,7 +153,7 @@ public function getRowUrl($row) } /** - * {@inheritdoc} + * @inheritdoc */ public function getHeadersVisibility() { diff --git a/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php new file mode 100644 index 0000000000000..280948439e1f8 --- /dev/null +++ b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\DataProviders; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Directory\Model\Country\Postcode\Config as PostCodeConfig; + +/** + * Provides postcodes patterns into template. + */ +class PostCodesPatternsAttributeData implements ArgumentInterface +{ + /** + * @var PostCodeConfig + */ + private $postCodeConfig; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * Constructor + * + * @param PostCodeConfig $postCodeConfig + * @param SerializerInterface $serializer + */ + public function __construct(PostCodeConfig $postCodeConfig, SerializerInterface $serializer) + { + $this->postCodeConfig = $postCodeConfig; + $this->serializer = $serializer; + } + + /** + * Get serialized post codes + * + * @return string + */ + public function getSerializedPostCodes(): string + { + return $this->serializer->serialize($this->postCodeConfig->getPostCodes()); + } +} diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php index 7b265ae1f0f32..d3d3306a49b44 100644 --- a/app/code/Magento/Customer/Block/Form/Login.php +++ b/app/code/Magento/Customer/Block/Form/Login.php @@ -47,15 +47,6 @@ public function __construct( $this->_customerSession = $customerSession; } - /** - * @return $this - */ - protected function _prepareLayout() - { - $this->pageConfig->getTitle()->set(__('Customer Login')); - return parent::_prepareLayout(); - } - /** * Retrieve form posting url * diff --git a/app/code/Magento/Customer/Block/Form/Register.php b/app/code/Magento/Customer/Block/Form/Register.php index 322dd2cbfe915..59966768a2eda 100644 --- a/app/code/Magento/Customer/Block/Form/Register.php +++ b/app/code/Magento/Customer/Block/Form/Register.php @@ -86,17 +86,6 @@ public function getConfig($path) return $this->_scopeConfig->getValue($path, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); } - /** - * Prepare layout - * - * @return $this - */ - protected function _prepareLayout() - { - $this->pageConfig->getTitle()->set(__('Create New Customer Account')); - return parent::_prepareLayout(); - } - /** * Retrieve form posting url * diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 936563d519823..55101fb82afd0 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -61,7 +61,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ public function _construct() { @@ -70,6 +70,8 @@ public function _construct() } /** + * Check if dob attribute enabled in system + * * @return bool */ public function isEnabled() @@ -79,6 +81,8 @@ public function isEnabled() } /** + * Check if dob attribute marked as required + * * @return bool */ public function isRequired() @@ -88,6 +92,8 @@ public function isRequired() } /** + * Set date + * * @param string $date * @return $this */ @@ -135,6 +141,8 @@ protected function applyOutputFilter($value) } /** + * Get day + * * @return string|bool */ public function getDay() @@ -143,6 +151,8 @@ public function getDay() } /** + * Get month + * * @return string|bool */ public function getMonth() @@ -151,6 +161,8 @@ public function getMonth() } /** + * Get year + * * @return string|bool */ public function getYear() @@ -168,6 +180,19 @@ public function getLabel() return __('Date of Birth'); } + /** + * Retrieve store attribute label + * + * @param string $attributeCode + * + * @return string + */ + public function getStoreLabel($attributeCode) + { + $attribute = $this->_getAttribute($attributeCode); + return $attribute ? __($attribute->getStoreLabel()) : ''; + } + /** * Create correct date field * diff --git a/app/code/Magento/Customer/Block/Widget/Gender.php b/app/code/Magento/Customer/Block/Widget/Gender.php index d03c64a54fb94..9df3f1072ce0c 100644 --- a/app/code/Magento/Customer/Block/Widget/Gender.php +++ b/app/code/Magento/Customer/Block/Widget/Gender.php @@ -64,6 +64,7 @@ public function _construct() /** * Check if gender attribute enabled in system + * * @return bool */ public function isEnabled() @@ -73,6 +74,7 @@ public function isEnabled() /** * Check if gender attribute marked as required + * * @return bool */ public function isRequired() @@ -80,6 +82,19 @@ public function isRequired() return $this->_getAttribute('gender') ? (bool)$this->_getAttribute('gender')->isRequired() : false; } + /** + * Retrieve store attribute label + * + * @param string $attributeCode + * + * @return string + */ + public function getStoreLabel($attributeCode) + { + $attribute = $this->_getAttribute($attributeCode); + return $attribute ? __($attribute->getStoreLabel()) : ''; + } + /** * Get current customer from session * @@ -92,6 +107,7 @@ public function getCustomer() /** * Returns options from gender attribute + * * @return OptionInterface[] */ public function getGenderOptions() diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php index d50045f4a4092..6f1b051af7465 100644 --- a/app/code/Magento/Customer/Block/Widget/Name.php +++ b/app/code/Magento/Customer/Block/Widget/Name.php @@ -55,7 +55,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ public function _construct() { @@ -245,10 +245,13 @@ public function getStoreLabel($attributeCode) */ public function getAttributeValidationClass($attributeCode) { - return $this->_addressHelper->getAttributeValidationClass($attributeCode); + $attributeMetadata = $this->_getAttribute($attributeCode); + return $attributeMetadata ? $attributeMetadata->getFrontendClass() : ''; } /** + * Check if attribute is required + * * @param string $attributeCode * @return bool */ @@ -259,6 +262,8 @@ private function _isAttributeRequired($attributeCode) } /** + * Check if attribute is visible + * * @param string $attributeCode * @return bool */ diff --git a/app/code/Magento/Customer/Block/Widget/Taxvat.php b/app/code/Magento/Customer/Block/Widget/Taxvat.php index e5c9c01dc3ac5..e35f04f592a43 100644 --- a/app/code/Magento/Customer/Block/Widget/Taxvat.php +++ b/app/code/Magento/Customer/Block/Widget/Taxvat.php @@ -63,4 +63,17 @@ public function isRequired() { return $this->_getAttribute('taxvat') ? (bool)$this->_getAttribute('taxvat')->isRequired() : false; } + + /** + * Retrieve store attribute label + * + * @param string $attributeCode + * + * @return string + */ + public function getStoreLabel($attributeCode) + { + $attribute = $this->_getAttribute($attributeCode); + return $attribute ? __($attribute->getStoreLabel()) : ''; + } } diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index 38bc52eac4266..4eb41cedea29a 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -7,6 +7,8 @@ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Model\AuthenticationInterface; use Magento\Customer\Model\Customer\Mapper; @@ -25,6 +27,7 @@ use Magento\Framework\Escaper; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Customer\Controller\AbstractAccount; use Magento\Framework\Phrase; @@ -85,6 +88,11 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, Http */ private $escaper; + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param Context $context * @param Session $customerSession @@ -93,6 +101,7 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, Http * @param Validator $formKeyValidator * @param CustomerExtractor $customerExtractor * @param Escaper|null $escaper + * @param AddressRegistry|null $addressRegistry */ public function __construct( Context $context, @@ -101,7 +110,8 @@ public function __construct( CustomerRepositoryInterface $customerRepository, Validator $formKeyValidator, CustomerExtractor $customerExtractor, - ?Escaper $escaper = null + ?Escaper $escaper = null, + AddressRegistry $addressRegistry = null ) { parent::__construct($context); $this->session = $customerSession; @@ -110,6 +120,7 @@ public function __construct( $this->formKeyValidator = $formKeyValidator; $this->customerExtractor = $customerExtractor; $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); } /** @@ -195,6 +206,9 @@ public function execute() // whether a customer enabled change password option $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); + // No need to validate customer address while editing customer profile + $this->disableAddressValidation($customerCandidateDataObject); + $this->customerRepository->save($customerCandidateDataObject); $this->getEmailNotification()->credentialsChanged( $customerCandidateDataObject, @@ -352,4 +366,18 @@ private function getCustomerMapper() } return $this->customerMapper; } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } } diff --git a/app/code/Magento/Customer/Controller/Address/File/Upload.php b/app/code/Magento/Customer/Controller/Address/File/Upload.php new file mode 100644 index 0000000000000..adb4c7abd1729 --- /dev/null +++ b/app/code/Magento/Customer/Controller/Address/File/Upload.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Address\File; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Model\FileUploader; +use Magento\Customer\Model\FileUploaderFactory; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Psr\Log\LoggerInterface; +use Magento\Customer\Model\FileProcessorFactory; + +/** + * Class for upload files for customer custom address attributes + */ +class Upload extends Action implements HttpPostActionInterface +{ + /** + * @var FileUploaderFactory + */ + private $fileUploaderFactory; + + /** + * @var AddressMetadataInterface + */ + private $addressMetadataService; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FileProcessorFactory + */ + private $fileProcessorFactory; + + /** + * @param Context $context + * @param FileUploaderFactory $fileUploaderFactory + * @param AddressMetadataInterface $addressMetadataService + * @param LoggerInterface $logger + * @param FileProcessorFactory $fileProcessorFactory + */ + public function __construct( + Context $context, + FileUploaderFactory $fileUploaderFactory, + AddressMetadataInterface $addressMetadataService, + LoggerInterface $logger, + FileProcessorFactory $fileProcessorFactory + ) { + $this->fileUploaderFactory = $fileUploaderFactory; + $this->addressMetadataService = $addressMetadataService; + $this->logger = $logger; + $this->fileProcessorFactory = $fileProcessorFactory; + parent::__construct($context); + } + + /** + * @inheritDoc + */ + public function execute() + { + try { + $requestedFiles = $this->getRequest()->getFiles('custom_attributes'); + if (empty($requestedFiles)) { + $result = $this->processError(__('No files for upload.')); + } else { + $attributeCode = key($requestedFiles); + $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode); + + /** @var FileUploader $fileUploader */ + $fileUploader = $this->fileUploaderFactory->create([ + 'attributeMetadata' => $attributeMetadata, + 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + 'scope' => CustomAttributesDataInterface::CUSTOM_ATTRIBUTES, + ]); + + $errors = $fileUploader->validate(); + if (true !== $errors) { + $errorMessage = implode('</br>', $errors); + $result = $this->processError(($errorMessage)); + } else { + $result = $fileUploader->upload(); + $this->moveTmpFileToSuitableFolder($result); + } + } + } catch (LocalizedException $e) { + $result = $this->processError($e->getMessage(), $e->getCode()); + } catch (\Exception $e) { + $this->logger->critical($e); + $result = $this->processError($e->getMessage(), $e->getCode()); + } + + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setData($result); + return $resultJson; + } + + /** + * Move file from temporary folder to the 'customer_address' media folder + * + * @param array $fileInfo + * @throws LocalizedException + */ + private function moveTmpFileToSuitableFolder(&$fileInfo) + { + $fileName = $fileInfo['file']; + $fileProcessor = $this->fileProcessorFactory + ->create(['entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS]); + + $newFilePath = $fileProcessor->moveTemporaryFile($fileName); + $fileInfo['file'] = $newFilePath; + $fileInfo['url'] = $fileProcessor->getViewUrl( + $newFilePath, + 'file' + ); + } + + /** + * Prepare result array for errors + * + * @param string $message + * @param int $code + * @return array + */ + private function processError($message, $code = 0) + { + $result = [ + 'error' => $message, + 'errorcode' => $code, + ]; + + return $result; + } +} diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php index 217af0abd7592..25618e3129160 100644 --- a/app/code/Magento/Customer/Controller/Address/FormPost.php +++ b/app/code/Magento/Customer/Controller/Address/FormPost.php @@ -26,6 +26,8 @@ use Magento\Framework\View\Result\PageFactory; /** + * Customer Address Form Post Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FormPost extends \Magento\Customer\Controller\Address implements HttpPostActionInterface @@ -120,8 +122,18 @@ protected function _extractAddress() \Magento\Customer\Api\Data\AddressInterface::class ); $addressDataObject->setCustomerId($this->_getSession()->getCustomerId()) - ->setIsDefaultBilling($this->getRequest()->getParam('default_billing', false)) - ->setIsDefaultShipping($this->getRequest()->getParam('default_shipping', false)); + ->setIsDefaultBilling( + $this->getRequest()->getParam( + 'default_billing', + isset($existingAddressData['default_billing']) ? $existingAddressData['default_billing'] : false + ) + ) + ->setIsDefaultShipping( + $this->getRequest()->getParam( + 'default_shipping', + isset($existingAddressData['default_shipping']) ? $existingAddressData['default_shipping'] : false + ) + ); return $addressDataObject; } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php index 7337d005a7323..b69410ecbfce7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php @@ -7,6 +7,7 @@ namespace Magento\Customer\Controller\Adminhtml\Customer; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\AddressRepositoryInterface; @@ -25,8 +26,15 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.NumberOfChildren) */ -class InvalidateToken extends \Magento\Customer\Controller\Adminhtml\Index +class InvalidateToken extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::invalidate_tokens'; + /** * @var CustomerTokenServiceInterface */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php index 15da8b20adbca..ab39ca098162f 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php @@ -8,8 +8,18 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; +/** + * Delete customer action. + */ class Delete extends \Magento\Customer\Controller\Adminhtml\Index implements HttpPostActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::delete'; + /** * Delete customer action * diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php index 3c3808d0a1ee6..7220de0356817 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php @@ -8,10 +8,13 @@ use Magento\Backend\App\Action; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\EmailNotificationInterface; use Magento\Customer\Ui\Component\Listing\AttributeRepository; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\ObjectManager; /** * Customer inline edit action @@ -62,6 +65,11 @@ class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionIn */ private $emailNotification; + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param Action\Context $context * @param CustomerRepositoryInterface $customerRepository @@ -69,6 +77,7 @@ class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionIn * @param \Magento\Customer\Model\Customer\Mapper $customerMapper * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param \Psr\Log\LoggerInterface $logger + * @param AddressRegistry|null $addressRegistry */ public function __construct( Action\Context $context, @@ -76,13 +85,15 @@ public function __construct( \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Customer\Model\Customer\Mapper $customerMapper, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + AddressRegistry $addressRegistry = null ) { $this->customerRepository = $customerRepository; $this->resultJsonFactory = $resultJsonFactory; $this->customerMapper = $customerMapper; $this->dataObjectHelper = $dataObjectHelper; $this->logger = $logger; + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); parent::__construct($context); } @@ -219,6 +230,8 @@ protected function updateDefaultBilling(array $data) protected function saveCustomer(CustomerInterface $customer) { try { + // No need to validate customer address during inline edit action + $this->disableAddressValidation($customer); $this->customerRepository->save($customer); } catch (\Magento\Framework\Exception\InputException $e) { $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); @@ -304,4 +317,18 @@ protected function getErrorWithCustomerId($errorText) { return '[Customer ID: ' . $this->getCustomer()->getId() . '] ' . __($errorText); } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php index a540ad9d7a70e..5a9c52bf9b1c0 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Model\Customer; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; @@ -52,6 +53,8 @@ protected function massAction(AbstractCollection $collection) // Verify customer exists $customer = $this->customerRepository->getById($customerId); $customer->setGroupId($this->getRequest()->getParam('group')); + // No need to validate customer and customer address during assigning customer to the group + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); $customersUpdated++; } @@ -65,4 +68,15 @@ protected function massAction(AbstractCollection $collection) return $resultRedirect; } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php index 334018a881f12..edaeea6a15eb2 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php @@ -18,6 +18,13 @@ */ class MassDelete extends AbstractMassAction implements HttpPostActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::delete'; + /** * @var CustomerRepositoryInterface */ @@ -40,8 +47,7 @@ public function __construct( } /** - * @param AbstractCollection $collection - * @return \Magento\Backend\Model\View\Result\Redirect + * @inheritdoc */ protected function massAction(AbstractCollection $collection) { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php index 3e6046b0d117f..1e4fa91cbf899 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php @@ -16,6 +16,13 @@ */ class ResetPassword extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::reset_password'; + /** * Reset password handler * diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index adb420f983006..38ed688a835bc 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -5,6 +5,15 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Address\Mapper; +use Magento\Customer\Model\AddressRegistry; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\DataObjectFactory as ObjectFactory; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; @@ -13,6 +22,8 @@ use Magento\Customer\Model\EmailNotificationInterface; use Magento\Customer\Model\Metadata\Form; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\ObjectManager; /** * Save customer action. @@ -26,6 +37,100 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index implements HttpP */ private $emailNotification; + /** + * @var AddressRegistry + */ + private $addressRegistry; + + /** + * Constructor + * + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\Registry $coreRegistry + * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory + * @param \Magento\Customer\Model\CustomerFactory $customerFactory + * @param \Magento\Customer\Model\AddressFactory $addressFactory + * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory + * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory + * @param \Magento\Customer\Helper\View $viewHelper + * @param \Magento\Framework\Math\Random $random + * @param CustomerRepositoryInterface $customerRepository + * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @param Mapper $addressMapper + * @param AccountManagementInterface $customerAccountManagement + * @param AddressRepositoryInterface $addressRepository + * @param CustomerInterfaceFactory $customerDataFactory + * @param AddressInterfaceFactory $addressDataFactory + * @param \Magento\Customer\Model\Customer\Mapper $customerMapper + * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor + * @param DataObjectHelper $dataObjectHelper + * @param ObjectFactory $objectFactory + * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory + * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param AddressRegistry|null $addressRegistry + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\Registry $coreRegistry, + \Magento\Framework\App\Response\Http\FileFactory $fileFactory, + \Magento\Customer\Model\CustomerFactory $customerFactory, + \Magento\Customer\Model\AddressFactory $addressFactory, + \Magento\Customer\Model\Metadata\FormFactory $formFactory, + \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory, + \Magento\Customer\Helper\View $viewHelper, + \Magento\Framework\Math\Random $random, + CustomerRepositoryInterface $customerRepository, + \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, + Mapper $addressMapper, + AccountManagementInterface $customerAccountManagement, + AddressRepositoryInterface $addressRepository, + CustomerInterfaceFactory $customerDataFactory, + AddressInterfaceFactory $addressDataFactory, + \Magento\Customer\Model\Customer\Mapper $customerMapper, + \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor, + DataObjectHelper $dataObjectHelper, + ObjectFactory $objectFactory, + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory, + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + AddressRegistry $addressRegistry = null + ) { + parent::__construct( + $context, + $coreRegistry, + $fileFactory, + $customerFactory, + $addressFactory, + $formFactory, + $subscriberFactory, + $viewHelper, + $random, + $customerRepository, + $extensibleDataObjectConverter, + $addressMapper, + $customerAccountManagement, + $addressRepository, + $customerDataFactory, + $addressDataFactory, + $customerMapper, + $dataObjectProcessor, + $dataObjectHelper, + $objectFactory, + $layoutFactory, + $resultLayoutFactory, + $resultPageFactory, + $resultForwardFactory, + $resultJsonFactory + ); + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + } + /** * Reformat customer account data to be compatible with customer service interface * @@ -184,17 +289,17 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData) public function execute() { $returnToEdit = false; - $originalRequestData = $this->getRequest()->getPostValue(); - $customerId = $this->getCurrentCustomerId(); - if ($originalRequestData) { + if ($this->getRequest()->getPostValue()) { try { // optional fields might be set in request for future processing by observers in other modules $customerData = $this->_extractCustomerData(); if ($customerId) { $currentCustomer = $this->_customerRepository->getById($customerId); + // No need to validate customer address while editing customer profile + $this->disableAddressValidation($currentCustomer); $customerData = array_merge( $this->customerMapper->toFlatArray($currentCustomer), $customerData @@ -257,7 +362,7 @@ public function execute() $messages = $exception->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (\Magento\Framework\Exception\AbstractAggregateException $exception) { $errors = $exception->getErrors(); @@ -266,18 +371,19 @@ public function execute() $messages[] = $error->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (LocalizedException $exception) { $this->_addSessionErrorMessages($exception->getMessage()); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (\Exception $exception) { $this->messageManager->addException($exception, __('Something went wrong while saving the customer.')); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } } + $resultRedirect = $this->resultRedirectFactory->create(); if ($returnToEdit) { if ($customerId) { @@ -368,4 +474,43 @@ private function getCurrentCustomerId() return $customerId; } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } + + /** + * Retrieve formatted form data + * + * @return array + */ + private function retrieveFormattedFormData(): array + { + $originalRequestData = $this->getRequest()->getPostValue(); + + /* Customer data filtration */ + if (isset($originalRequestData['customer'])) { + $customerData = $this->_extractData( + 'adminhtml_customer', + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + [], + 'customer' + ); + + $customerData = array_intersect_key($customerData, $originalRequestData['customer']); + $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); + } + + return $originalRequestData; + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php index 20d330354bce4..02a045086224c 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Customer\Api\AccountManagementInterface; @@ -17,7 +19,10 @@ use Magento\Framework\DataObjectFactory; /** + * Class Viewfile serves to show file or image by file/image name provided in request parameters. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.AllPurposeAction) */ class Viewfile extends \Magento\Customer\Controller\Adminhtml\Index { @@ -127,8 +132,6 @@ public function __construct( * * @return \Magento\Framework\Controller\ResultInterface|void * @throws NotFoundException - * - * @SuppressWarnings(PHPMD.ExitExpression) */ public function execute() { @@ -146,6 +149,7 @@ public function execute() } if ($plain) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $extension = pathinfo($path, PATHINFO_EXTENSION); switch (strtolower($extension)) { case 'gif': @@ -175,6 +179,7 @@ public function execute() $resultRaw->setContents($directory->readFile($fileName)); return $resultRaw; } else { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $name = pathinfo($path, PATHINFO_BASENAME); $this->_fileFactory->create( $name, diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php index aa73e275ee0ca..f82a4d15ae8bf 100644 --- a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php +++ b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php @@ -5,10 +5,13 @@ */ namespace Magento\Customer\CustomerData\Plugin; -use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\Cookie\PhpCookieManager; +/** + * Class SessionChecker + */ class SessionChecker { /** @@ -36,10 +39,12 @@ public function __construct( /** * Delete frontend session cookie if customer session is expired * - * @param SessionManager $sessionManager + * @param SessionManagerInterface $sessionManager * @return void + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException */ - public function beforeStart(SessionManager $sessionManager) + public function beforeStart(SessionManagerInterface $sessionManager) { if (!$this->cookieManager->getCookie($sessionManager->getName()) && $this->cookieManager->getCookie('mage-cache-sessid') diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index 7c81e29325823..765c13b287704 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -417,23 +417,4 @@ public function isAttributeVisible($code) } return false; } - - /** - * Retrieve attribute required - * - * @param string $code - * @return bool - * @throws NoSuchEntityException - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function isAttributeRequired($code) - { - $attributeMetadata = $this->_addressMetadataService->getAttributeMetadata($code); - - if ($attributeMetadata) { - return $attributeMetadata->isRequired(); - } - - return false; - } } diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index d7c5d7f47a4cf..250d190f8fae7 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -60,6 +60,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AccountManagement implements AccountManagementInterface { @@ -524,6 +525,8 @@ private function activateCustomer($customer, $confirmationKey) } $customer->setConfirmation(null); + // No need to validate customer and customer address while activating customer + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); $this->getEmailNotification()->newAccount( $customer, @@ -621,7 +624,6 @@ public function initiatePasswordReset($email, $template, $websiteId = null) * @param string $rpToken * @throws ExpiredException * @throws NoSuchEntityException - * * @return CustomerInterface * @throws LocalizedException */ @@ -683,8 +685,9 @@ public function resetPassword($email, $resetToken, $newPassword) $customer = $this->customerRepository->get($email); } - // No need to validate customer address while saving customer reset password token + // No need to validate customer and customer address while saving customer reset password token $this->disableAddressValidation($customer); + $this->setIgnoreValidationFlag($customer); //Validate Token and new password strength $this->validateResetPasswordToken($customer->getId(), $resetToken); @@ -699,7 +702,12 @@ public function resetPassword($email, $resetToken, $newPassword) $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); $this->destroyCustomerSessions($customer->getId()); - $this->sessionManager->destroy(); + if ($this->sessionManager->isSessionExists()) { + //delete old session and move data to the new session + //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session + // phpcs:ignore Magento2.Functions.DiscouragedFunction + session_regenerate_id(true); + } $this->customerRepository->save($customer); return true; @@ -811,7 +819,7 @@ public function getConfirmationStatus($customerId) /** * @inheritdoc */ - public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '', $extensions = []) + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') { if ($password !== null) { $this->checkPasswordStrength($password); @@ -827,7 +835,7 @@ public function createAccount(CustomerInterface $customer, $password = null, $re } else { $hash = null; } - return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl, $extensions); + return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl); } /** @@ -835,12 +843,8 @@ public function createAccount(CustomerInterface $customer, $password = null, $re * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function createAccountWithPasswordHash( - CustomerInterface $customer, - $hash, - $redirectUrl = '', - $extensions = [] - ) { + public function createAccountWithPasswordHash(CustomerInterface $customer, $hash, $redirectUrl = '') + { // This logic allows an existing customer to be added to a different store. No new account is created. // The plan is to move this logic into a new method called something like 'registerAccountWithStore' if ($customer->getId()) { @@ -913,7 +917,7 @@ public function createAccountWithPasswordHash( $customer = $this->customerRepository->getById($customer->getId()); $newLinkToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newLinkToken); - $this->sendEmailConfirmation($customer, $redirectUrl, $extensions); + $this->sendEmailConfirmation($customer, $redirectUrl); return $customer; } @@ -941,12 +945,11 @@ public function getDefaultShippingAddress($customerId) * * @param CustomerInterface $customer * @param string $redirectUrl - * @param array $extensions * @return void * @throws LocalizedException * @throws NoSuchEntityException */ - protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl, $extensions = []) + protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl) { try { $hash = $this->customerRegistry->retrieveSecureData($customer->getId())->getPasswordHash(); @@ -956,14 +959,7 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU } elseif ($hash == '') { $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } - $this->getEmailNotification()->newAccount( - $customer, - $templateType, - $redirectUrl, - $customer->getStoreId(), - null, - $extensions - ); + $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); @@ -1029,6 +1025,7 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass $this->checkPasswordStrength($newPassword); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); $this->destroyCustomerSessions($customer->getId()); + $this->disableAddressValidation($customer); $this->customerRepository->save($customer); return true; @@ -1571,6 +1568,7 @@ private function getEmailNotification() /** * Destroy all active customer sessions by customer id (current session will not be destroyed). + * * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering * configured session lifetime. * diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php index 4976ec546609f..ea9b103f42273 100644 --- a/app/code/Magento/Customer/Model/Address.php +++ b/app/code/Magento/Customer/Model/Address.php @@ -122,7 +122,7 @@ public function __construct( } /** - * Init model + * Initialize address model * * @return void */ @@ -167,17 +167,21 @@ public function updateData(AddressInterface $address) } /** - * @inheritdoc + * Create address data object based on current address model. + * + * @param int|null $defaultBillingAddressId + * @param int|null $defaultShippingAddressId + * @return AddressInterface + * Use Api/Data/AddressInterface as a result of service operations. Don't rely on the model to provide + * the instance of Api/Data/AddressInterface + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getDataModel($defaultBillingAddressId = null, $defaultShippingAddressId = null) { if ($this->getCustomerId() || $this->getParentId()) { - if ($this->getCustomer()->getDefaultBillingAddress()) { - $defaultBillingAddressId = $this->getCustomer()->getDefaultBillingAddress()->getId(); - } - if ($this->getCustomer()->getDefaultShippingAddress()) { - $defaultShippingAddressId = $this->getCustomer()->getDefaultShippingAddress()->getId(); - } + $customer = $this->getCustomer(); + $defaultBillingAddressId = $customer->getDefaultBilling() ?: $defaultBillingAddressId; + $defaultShippingAddressId = $customer->getDefaultShipping() ?: $defaultShippingAddressId; } return parent::getDataModel($defaultBillingAddressId, $defaultShippingAddressId); } @@ -260,7 +264,7 @@ public function getDefaultAttributeCodes() } /** - * Clone object handler + * Clone address * * @return void */ @@ -359,7 +363,11 @@ public function reindex() } /** - * @inheritdoc + * Get a list of custom attribute codes. + * + * By default, entity can be extended only using extension attributes functionality. + * + * @return string[] * @since 100.0.6 */ protected function getCustomAttributesCodes() diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index 146fec4c79f46..d8d0646b30bb8 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -222,7 +222,7 @@ public function getStreet() } /** - * Get steet line by number + * Get street line by number * * @param int $number * @return string diff --git a/app/code/Magento/Customer/Model/Address/AddressModelInterface.php b/app/code/Magento/Customer/Model/Address/AddressModelInterface.php index 0af36e877555f..06de3a99a831c 100644 --- a/app/code/Magento/Customer/Model/Address/AddressModelInterface.php +++ b/app/code/Magento/Customer/Model/Address/AddressModelInterface.php @@ -15,7 +15,7 @@ interface AddressModelInterface { /** - * Get steet line by number + * Get street line by number * * @param int $number * @return string diff --git a/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php new file mode 100644 index 0000000000000..d6e63e11ee453 --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Address; + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Eav\Api\AttributeOptionManagementInterface; + +/** + * Provides customer address data. + */ +class CustomAttributesProcessor +{ + /** + * @var AddressMetadataInterface + */ + private $addressMetadata; + + /** + * @var AttributeOptionManagementInterface + */ + private $attributeOptionManager; + + /** + * @param AddressMetadataInterface $addressMetadata + * @param AttributeOptionManagementInterface $attributeOptionManager + */ + public function __construct( + AddressMetadataInterface $addressMetadata, + AttributeOptionManagementInterface $attributeOptionManager + ) { + $this->addressMetadata = $addressMetadata; + $this->attributeOptionManager = $attributeOptionManager; + } + + /** + * Set Labels to custom Attributes + * + * @param \Magento\Framework\Api\AttributeValue[] $customAttributes + * @return array $customAttributes + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\StateException + */ + private function setLabelsForAttributes(array $customAttributes): array + { + if (!empty($customAttributes)) { + foreach ($customAttributes as $customAttributeCode => $customAttribute) { + $attributeOptionLabels = $this->getAttributeLabels($customAttribute, $customAttributeCode); + if (!empty($attributeOptionLabels)) { + $customAttributes[$customAttributeCode]['label'] = implode(', ', $attributeOptionLabels); + } + } + } + + return $customAttributes; + } + /** + * Get Labels by CustomAttribute and CustomAttributeCode + * + * @param array $customAttribute + * @param string $customAttributeCode + * @return array $attributeOptionLabels + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\StateException + */ + private function getAttributeLabels(array $customAttribute, string $customAttributeCode) : array + { + $attributeOptionLabels = []; + + if (!empty($customAttribute['value'])) { + $customAttributeValues = explode(',', $customAttribute['value']); + $attributeOptions = $this->attributeOptionManager->getItems( + \Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, + $customAttributeCode + ); + + if (!empty($attributeOptions)) { + foreach ($attributeOptions as $attributeOption) { + $attributeOptionValue = $attributeOption->getValue(); + if (\in_array($attributeOptionValue, $customAttributeValues, false)) { + $attributeOptionLabels[] = $attributeOption->getLabel() ?? $attributeOptionValue; + } + } + } + } + + return $attributeOptionLabels; + } + + /** + * Filter not visible on storefront custom attributes. + * + * @param array $attributes + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function filterNotVisibleAttributes(array $attributes): array + { + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + unset($attributes[$attributeMetadata->getAttributeCode()]); + } + } + + return $this->setLabelsForAttributes($attributes); + } +} diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php new file mode 100644 index 0000000000000..9202d7492040c --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Address; + +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Model\Address\Mapper as AddressMapper; +use Magento\Customer\Model\Address\Config as AddressConfig; + +/** + * Provides method to format customer address data. + */ +class CustomerAddressDataFormatter +{ + /** + * @var AddressMapper + */ + private $addressMapper; + + /** + * @var AddressConfig + */ + private $addressConfig; + + /** + * @var CustomAttributesProcessor + */ + private $customAttributesProcessor; + + /** + * @param Mapper $addressMapper + * @param Config $addressConfig + * @param CustomAttributesProcessor $customAttributesProcessor + */ + public function __construct( + AddressMapper $addressMapper, + AddressConfig $addressConfig, + CustomAttributesProcessor $customAttributesProcessor + ) { + $this->addressMapper = $addressMapper; + $this->addressConfig = $addressConfig; + $this->customAttributesProcessor = $customAttributesProcessor; + } + + /** + * Prepare customer address data. + * + * @param AddressInterface $customerAddress + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function prepareAddress(AddressInterface $customerAddress) + { + $resultAddress = [ + 'id' => $customerAddress->getId(), + 'customer_id' => $customerAddress->getCustomerId(), + 'company' => $customerAddress->getCompany(), + 'prefix' => $customerAddress->getPrefix(), + 'firstname' => $customerAddress->getFirstname(), + 'lastname' => $customerAddress->getLastname(), + 'middlename' => $customerAddress->getMiddlename(), + 'suffix' => $customerAddress->getSuffix(), + 'street' => $customerAddress->getStreet(), + 'city' => $customerAddress->getCity(), + 'region' => [ + 'region' => $customerAddress->getRegion()->getRegion(), + 'region_code' => $customerAddress->getRegion()->getRegionCode(), + 'region_id' => $customerAddress->getRegion()->getRegionId(), + ], + 'region_id' => $customerAddress->getRegionId(), + 'postcode' => $customerAddress->getPostcode(), + 'country_id' => $customerAddress->getCountryId(), + 'telephone' => $customerAddress->getTelephone(), + 'fax' => $customerAddress->getFax(), + 'default_billing' => $customerAddress->isDefaultBilling(), + 'default_shipping' => $customerAddress->isDefaultShipping(), + 'inline' => $this->getCustomerAddressInline($customerAddress), + 'custom_attributes' => [], + 'extension_attributes' => $customerAddress->getExtensionAttributes(), + ]; + + if ($customerAddress->getCustomAttributes()) { + $customerAddress = $customerAddress->__toArray(); + $resultAddress['custom_attributes'] = $this->customAttributesProcessor->filterNotVisibleAttributes( + $customerAddress['custom_attributes'] + ); + } + + return $resultAddress; + } + + /** + * Set additional customer address data + * + * @param AddressInterface $address + * @return string + */ + private function getCustomerAddressInline(AddressInterface $address): string + { + $builtOutputAddressData = $this->addressMapper->toFlatArray($address); + return $this->addressConfig + ->getFormatByCode(AddressConfig::DEFAULT_ADDRESS_FORMAT) + ->getRenderer() + ->renderArray($builtOutputAddressData); + } +} diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php new file mode 100644 index 0000000000000..04fdd2a7f7266 --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Address; + +/** + * Provides customer address data. + */ +class CustomerAddressDataProvider +{ + /** + * Customer addresses. + * + * @var array + */ + private $customerAddresses = []; + + /** + * @var CustomerAddressDataFormatter + */ + private $customerAddressDataFormatter; + + /** + * @param CustomerAddressDataFormatter $customerAddressDataFormatter + */ + public function __construct( + CustomerAddressDataFormatter $customerAddressDataFormatter + ) { + $this->customerAddressDataFormatter = $customerAddressDataFormatter; + } + + /** + * Get addresses for customer. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getAddressDataByCustomer( + \Magento\Customer\Api\Data\CustomerInterface $customer + ): array { + if (!empty($this->customerAddresses)) { + return $this->customerAddresses; + } + + $customerOriginAddresses = $customer->getAddresses(); + if (!$customerOriginAddresses) { + return []; + } + + $customerAddresses = []; + foreach ($customerOriginAddresses as $address) { + $customerAddresses[$address->getId()] = $this->customerAddressDataFormatter->prepareAddress($address); + } + + $this->customerAddresses = $customerAddresses; + + return $this->customerAddresses; + } +} diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php b/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php index 380b8a4d3446f..b1602e8ca1939 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Model\Attribute\Data; use Magento\Directory\Helper\Data as DirectoryHelper; @@ -13,7 +14,8 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface as MagentoTimezone; /** - * Customer Address Postal/Zip Code Attribute Data Model + * Customer Address Postal/Zip Code Attribute Data Model. + * * This Data Model Has to Be Set Up in additional EAV attribute table */ class Postcode extends \Magento\Eav\Model\Attribute\Data\AbstractData @@ -40,7 +42,8 @@ public function __construct( } /** - * Validate postal/zip code + * Validate postal/zip code. + * * Return true and skip validation if country zip code is optional * * @param array|string $value @@ -104,7 +107,7 @@ public function restoreValue($value) } /** - * Return formated attribute value from entity model + * Return formatted attribute value from entity model * * @param string $format * @return string|array diff --git a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php index fc0fa3ebc073d..40a10a1db0935 100644 --- a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php +++ b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php @@ -87,15 +87,20 @@ public function afterDelete() { $result = parent::afterDelete(); - if ($this->getScope() == \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES) { - $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); - $website = $this->_storeManager->getWebsite($this->getScopeCode()); - $attribute->setWebsite($website); - $attribute->load($attribute->getId()); - $attribute->setData('scope_multiline_count', null); - $attribute->save(); - } + $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); + switch ($this->getScope()) { + case \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES: + $website = $this->_storeManager->getWebsite($this->getScopeCode()); + $attribute->setWebsite($website); + $attribute->load($attribute->getId()); + $attribute->setData('scope_multiline_count', null); + break; + case ScopeConfigInterface::SCOPE_TYPE_DEFAULT: + $attribute->setData('multiline_count', 2); + break; + } + $attribute->save(); return $result; } } diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index 972cb63ed452e..1287dbe5df708 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -219,6 +219,13 @@ class Customer extends \Magento\Framework\Model\AbstractModel */ private $accountConfirmation; + /** + * Caching property to store customer address data models by the address ID. + * + * @var array + */ + private $storedAddress; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -314,7 +321,10 @@ public function getDataModel() $addressesData = []; /** @var \Magento\Customer\Model\Address $address */ foreach ($this->getAddresses() as $address) { - $addressesData[] = $address->getDataModel(); + if (!isset($this->storedAddress[$address->getId()])) { + $this->storedAddress[$address->getId()] = $address->getDataModel(); + } + $addressesData[] = $this->storedAddress[$address->getId()]; } $customerDataObject = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( @@ -1044,17 +1054,6 @@ public function resetErrors() return $this; } - /** - * Prepare customer for delete - * - * @return $this - */ - public function beforeDelete() - { - //TODO : Revisit and figure handling permissions in MAGETWO-11084 Implementation: Service Context Provider - return parent::beforeDelete(); - } - /** * Processing object after save data * @@ -1295,7 +1294,7 @@ public function getResetPasswordLinkExpirationPeriod() } /** - * Create address instance + * Create Address from Factory * * @return Address */ @@ -1305,7 +1304,7 @@ protected function _createAddressInstance() } /** - * Create address collection instance + * Create Address Collection from Factory * * @return \Magento\Customer\Model\ResourceModel\Address\Collection */ @@ -1315,7 +1314,7 @@ protected function _createAddressCollection() } /** - * Returns templates types + * Get Template Types * * @return array */ diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 30a9dbedde8d0..144c24f8e8355 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -17,7 +17,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * Class for notification customer. + * Customer email notification * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -65,8 +65,6 @@ class EmailNotification implements EmailNotificationInterface self::NEW_ACCOUNT_EMAIL_CONFIRMATION => self::XML_PATH_CONFIRM_EMAIL_TEMPLATE, ]; - const CUSTOMER_CONFIRM_URL = 'customer/account/confirm/'; - /**#@-*/ /**#@-*/ @@ -366,7 +364,6 @@ public function passwordResetConfirmation(CustomerInterface $customer) * @param string $backUrl * @param string $storeId * @param string $sendemailStoreId - * @param array $extensions * @return void * @throws LocalizedException */ @@ -375,8 +372,7 @@ public function newAccount( $type = self::NEW_ACCOUNT_EMAIL_REGISTERED, $backUrl = '', $storeId = 0, - $sendemailStoreId = null, - $extensions = [] + $sendemailStoreId = null ) { $types = self::TEMPLATE_TYPES; @@ -394,26 +390,11 @@ public function newAccount( $customerEmailData = $this->getFullCustomerObject($customer); - $templateVars = [ - 'customer' => $customerEmailData, - 'back_url' => $backUrl, - 'store' => $store - ]; - if ($type == self::NEW_ACCOUNT_EMAIL_CONFIRMATION) { - if (empty($extensions)) { - $templateVars['url'] = self::CUSTOMER_CONFIRM_URL; - $templateVars['extensions'] = $extensions; - } else { - $templateVars['url'] = $extensions['url']; - $templateVars['extensions'] = $extensions['extension_info']; - } - } - $this->sendEmailTemplate( $customer, $types[$type], self::XML_PATH_REGISTER_EMAIL_IDENTITY, - $templateVars, + ['customer' => $customerEmailData, 'back_url' => $backUrl, 'store' => $store], $storeId ); } diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index 6a8472758c169..c16faea284296 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model; +/** + * Processor class for work with uploaded files + */ class FileProcessor { /** @@ -232,7 +237,7 @@ public function moveTemporaryFile($fileName) ); } - $fileName = $dispersionPath . '/' . $fileName; + $fileName = $dispersionPath . '/' . $destinationFileName; return $fileName; } diff --git a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php index d4e0c5cce3401..336e7ab770b02 100644 --- a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php +++ b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php @@ -11,6 +11,9 @@ use Magento\Framework\Indexer\DimensionFactory; use Magento\Framework\Indexer\DimensionProviderInterface; +/** + * Class CustomerGroupDimensionProvider + */ class CustomerGroupDimensionProvider implements DimensionProviderInterface { /** @@ -34,12 +37,19 @@ class CustomerGroupDimensionProvider implements DimensionProviderInterface */ private $dimensionFactory; + /** + * @param CustomerGroupCollectionFactory $collectionFactory + * @param DimensionFactory $dimensionFactory + */ public function __construct(CustomerGroupCollectionFactory $collectionFactory, DimensionFactory $dimensionFactory) { $this->dimensionFactory = $dimensionFactory; $this->collectionFactory = $collectionFactory; } + /** + * @inheritdoc + */ public function getIterator(): \Traversable { foreach ($this->getCustomerGroups() as $customerGroup) { @@ -48,6 +58,8 @@ public function getIterator(): \Traversable } /** + * Get Customer Groups + * * @return array */ private function getCustomerGroups(): array diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php index 5a46fdb9defc4..8e64fba4a9b08 100644 --- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php +++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Cache for attribute metadata @@ -53,6 +54,11 @@ class AttributeMetadataCache */ private $serializer; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Constructor * @@ -60,17 +66,21 @@ class AttributeMetadataCache * @param StateInterface $state * @param SerializerInterface $serializer * @param AttributeMetadataHydrator $attributeMetadataHydrator + * @param StoreManagerInterface $storeManager */ public function __construct( CacheInterface $cache, StateInterface $state, SerializerInterface $serializer, - AttributeMetadataHydrator $attributeMetadataHydrator + AttributeMetadataHydrator $attributeMetadataHydrator, + StoreManagerInterface $storeManager = null ) { $this->cache = $cache; $this->state = $state; $this->serializer = $serializer; $this->attributeMetadataHydrator = $attributeMetadataHydrator; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** @@ -82,11 +92,12 @@ public function __construct( */ public function load($entityType, $suffix = '') { - if (isset($this->attributes[$entityType . $suffix])) { - return $this->attributes[$entityType . $suffix]; + $storeId = $this->storeManager->getStore()->getId(); + if (isset($this->attributes[$entityType . $suffix . $storeId])) { + return $this->attributes[$entityType . $suffix . $storeId]; } if ($this->isEnabled()) { - $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedData = $this->cache->load($cacheKey); if ($serializedData) { $attributesData = $this->serializer->unserialize($serializedData); @@ -94,7 +105,7 @@ public function load($entityType, $suffix = '') foreach ($attributesData as $key => $attributeData) { $attributes[$key] = $this->attributeMetadataHydrator->hydrate($attributeData); } - $this->attributes[$entityType . $suffix] = $attributes; + $this->attributes[$entityType . $suffix . $storeId] = $attributes; return $attributes; } } @@ -111,9 +122,10 @@ public function load($entityType, $suffix = '') */ public function save($entityType, array $attributes, $suffix = '') { - $this->attributes[$entityType . $suffix] = $attributes; + $storeId = $this->storeManager->getStore()->getId(); + $this->attributes[$entityType . $suffix . $storeId] = $attributes; if ($this->isEnabled()) { - $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $attributesData = []; foreach ($attributes as $key => $attribute) { $attributesData[$key] = $this->attributeMetadataHydrator->extract($attribute); diff --git a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php index 168f00be16e33..8e443e93354b0 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php @@ -12,6 +12,8 @@ use Magento\Framework\Validator\EmailAddress; /** + * Form Element Abstract Data Model + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractData @@ -137,6 +139,7 @@ public function setRequestScope($scope) /** * Set scope visibility + * * Search value only in scope or search value in scope and global * * @param boolean $flag @@ -281,9 +284,14 @@ protected function _validateInputRule($value) ); if ($inputValidation !== null) { + $allowWhiteSpace = false; + switch ($inputValidation) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // Continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php index 7747e309d82a6..71e70f8e14208 100644 --- a/app/code/Magento/Customer/Model/Options.php +++ b/app/code/Magento/Customer/Model/Options.php @@ -8,7 +8,11 @@ use Magento\Config\Model\Config\Source\Nooptreq as NooptreqSource; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Framework\Escaper; +use Magento\Store\Api\Data\StoreInterface; +/** + * Customer Options. + */ class Options { /** @@ -38,7 +42,7 @@ public function __construct( /** * Retrieve name prefix dropdown options * - * @param null $store + * @param null|string|bool|int|StoreInterface $store * @return array|bool */ public function getNamePrefixOptions($store = null) @@ -52,7 +56,7 @@ public function getNamePrefixOptions($store = null) /** * Retrieve name suffix dropdown options * - * @param null $store + * @param null|string|bool|int|StoreInterface $store * @return array|bool */ public function getNameSuffixOptions($store = null) @@ -64,7 +68,9 @@ public function getNameSuffixOptions($store = null) } /** - * @param $options + * Unserialize and clear name prefix or suffix options. + * + * @param string $options * @param bool $isOptional * @return array|bool * @@ -78,6 +84,7 @@ protected function _prepareNamePrefixSuffixOptions($options, $isOptional = false /** * Unserialize and clear name prefix or suffix options + * * If field is optional, add an empty first option. * * @param string $options @@ -91,7 +98,7 @@ private function prepareNamePrefixSuffixOptions($options, $isOptional = false) return false; } $result = []; - $options = explode(';', $options); + $options = array_filter(explode(';', $options)); foreach ($options as $value) { $value = $this->escaper->escapeHtml(trim($value)); $result[$value] = $value; diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php index a52c372310843..200eaabe6517d 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php @@ -14,6 +14,7 @@ /** * Class Address + * * @package Magento\Customer\Model\ResourceModel * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -31,8 +32,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity /** * @param \Magento\Eav\Model\Entity\Context $context - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite * @param \Magento\Framework\Validator\Factory $validatorFactory * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data @@ -98,6 +99,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $address) */ protected function _validate($address) { + if ($address->getDataByKey('should_ignore_validation')) { + return; + }; $validator = $this->_validatorFactory->createValidator('customer_address', 'save'); if (!$validator->isValid($address)) { @@ -110,7 +114,7 @@ protected function _validate($address) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -120,6 +124,8 @@ public function delete($object) } /** + * Get instance of DeleteRelation class + * * @deprecated 100.2.0 * @return DeleteRelation */ @@ -129,6 +135,8 @@ private function getDeleteRelation() } /** + * Get instance of CustomerRegistry class + * * @deprecated 100.2.0 * @return CustomerRegistry */ @@ -138,6 +146,8 @@ private function getCustomerRegistry() } /** + * After delete entity process + * * @param \Magento\Customer\Model\Address $address * @return $this */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index b25838e245488..ddc0576cb0dae 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -171,7 +171,14 @@ public function __construct( } /** - * @inheritdoc + * Create or update a customer. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param string $passwordHash + * @return \Magento\Customer\Api\Data\CustomerInterface + * @throws \Magento\Framework\Exception\InputException If bad input is provided + * @throws \Magento\Framework\Exception\State\InputMismatchException If the provided email is already used + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -304,7 +311,13 @@ private function populateCustomerWithSecureData($customerModel, $passwordHash = } /** - * @inheritdoc + * Retrieve customer. + * + * @param string $email + * @param int|null $websiteId + * @return \Magento\Customer\Api\Data\CustomerInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified email does not exist. + * @throws \Magento\Framework\Exception\LocalizedException */ public function get($email, $websiteId = null) { @@ -313,7 +326,12 @@ public function get($email, $websiteId = null) } /** - * @inheritdoc + * Get customer by Customer ID. + * + * @param int $customerId + * @return \Magento\Customer\Api\Data\CustomerInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified ID does not exist. + * @throws \Magento\Framework\Exception\LocalizedException */ public function getById($customerId) { @@ -322,7 +340,15 @@ public function getById($customerId) } /** - * @inheritdoc + * Retrieve customers which match a specified criteria. + * + * This call returns an array of objects, but detailed information about each object’s attributes might not be + * included. See http://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine + * which call to use to get detailed information about all attributes for an object. + * + * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria + * @return \Magento\Customer\Api\Data\CustomerSearchResultsInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function getList(SearchCriteriaInterface $searchCriteria) { @@ -362,7 +388,11 @@ public function getList(SearchCriteriaInterface $searchCriteria) } /** - * @inheritdoc + * Delete customer. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @return bool true on success + * @throws \Magento\Framework\Exception\LocalizedException */ public function delete(CustomerInterface $customer) { @@ -370,7 +400,12 @@ public function delete(CustomerInterface $customer) } /** - * @inheritdoc + * Delete customer by Customer ID. + * + * @param int $customerId + * @return bool true on success + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException */ public function deleteById($customerId) { diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group.php b/app/code/Magento/Customer/Model/ResourceModel/Group.php index 80203e742e09a..987723c5c9f58 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Group.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Group.php @@ -29,8 +29,8 @@ class Group extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Abs /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Snapshot $entitySnapshot, - * @param RelationComposite $entityRelationComposite, + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param Customer\CollectionFactory $customersFactory * @param string $connectionName @@ -110,6 +110,8 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $group) } /** + * Create customers collection. + * * @return \Magento\Customer\Model\ResourceModel\Customer\Collection */ protected function _createCustomersCollection() @@ -131,7 +133,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $group) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php index f608a6cf4c11c..123a9eef4b75a 100644 --- a/app/code/Magento/Customer/Model/Vat.php +++ b/app/code/Magento/Customer/Model/Vat.php @@ -179,18 +179,21 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = return $gatewayResponse; } + $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode); + $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode); + try { $soapClient = $this->createVatNumberValidationSoapClient(); $requestParams = []; - $requestParams['countryCode'] = $countryCode; + $requestParams['countryCode'] = $countryCodeForVatNumber; $vatNumberSanitized = $this->isCountryInEU($countryCode) - ? str_replace([' ', '-', $countryCode], ['', '', ''], $vatNumber) + ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber) : str_replace([' ', '-'], ['', ''], $vatNumber); $requestParams['vatNumber'] = $vatNumberSanitized; - $requestParams['requesterCountryCode'] = $requesterCountryCode; + $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber; $reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode) - ? str_replace([' ', '-', $requesterCountryCode], ['', '', ''], $requesterVatNumber) + ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber) : str_replace([' ', '-'], ['', ''], $requesterVatNumber); $requestParams['requesterVatNumber'] = $reqVatNumSanitized; // Send request to service @@ -301,4 +304,22 @@ public function isCountryInEU($countryCode, $storeId = null) ); return in_array($countryCode, $euCountries); } + + /** + * Returns the country code to use in the VAT number which is not always the same as the normal country code + * + * @param string $countryCode + * @return string + */ + private function getCountryCodeForVatNumber(string $countryCode): string + { + // Greece uses a different code for VAT numbers then its country code + // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 + // And https://en.wikipedia.org/wiki/VAT_identification_number: + // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code + // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language, + // instead of its ISO 3166-1 alpha-2 country code GR)" + + return $countryCode === 'GR' ? 'EL' : $countryCode; + } } diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php index 9caa2988c5a94..4f129f05aa82c 100644 --- a/app/code/Magento/Customer/Model/Visitor.php +++ b/app/code/Magento/Customer/Model/Visitor.php @@ -14,6 +14,7 @@ * * @package Magento\Customer\Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Visitor extends \Magento\Framework\Model\AbstractModel { @@ -168,10 +169,6 @@ public function initByRequest($observer) $this->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)); - // prevent saving Visitor for safe methods, e.g. GET request - if ($this->requestSafety->isSafeMethod()) { - return $this; - } if (!$this->getId()) { $this->setSessionId($this->session->getSessionId()); $this->save(); diff --git a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php index eb7e81009c92c..26c4c50009bb1 100644 --- a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php +++ b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php @@ -6,11 +6,15 @@ namespace Magento\Customer\Observer; +use Magento\Customer\Model\Customer; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\CustomerRegistry; +/** + * Class observer UpgradeCustomerPasswordObserver to upgrade customer password hash when customer has logged in + */ class UpgradeCustomerPasswordObserver implements ObserverInterface { /** @@ -61,7 +65,20 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (!$this->encryptor->validateHashVersion($customerSecure->getPasswordHash(), true)) { $customerSecure->setPasswordHash($this->encryptor->getHash($password, true)); + // No need to validate customer and customer address while upgrading customer password + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); } } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertAddressInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertAddressInCustomersAddressGridActionGroup.xml new file mode 100644 index 0000000000000..53dba774d6c43 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertAddressInCustomersAddressGridActionGroup.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"> + <!--Assert customer info in customers grid row --> + <actionGroup name="AdminAssertAddressInCustomersAddressGrid"> + <arguments> + <argument name="text" type="string"/> + </arguments> + <see selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" userInput="{{text}}" stepKey="seeTextInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerAccountInformationActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerAccountInformationActionGroup.xml new file mode 100644 index 0000000000000..a908d042fcc59 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerAccountInformationActionGroup.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"> + <!-- Assert Customer Account Information --> + <actionGroup name="AdminAssertCustomerAccountInformation" > + <arguments> + <argument name="firstName" type="string" defaultValue=""/> + <argument name="lastName" type="string" defaultValue=""/> + <argument name="email" type="string" defaultValue=""/> + </arguments> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="proceedToAccountInformation"/> + <seeInField userInput="{{firstName}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="firstName"/> + <seeInField userInput="{{lastName}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="lastName"/> + <seeInField userInput="{{email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="email"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultBillingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultBillingAddressActionGroup.xml new file mode 100644 index 0000000000000..32b624706102f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultBillingAddressActionGroup.xml @@ -0,0 +1,30 @@ +<?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"> + <!-- Assert Customer Default Billing Address --> + <actionGroup name="AdminAssertCustomerDefaultBillingAddress" > + <arguments> + <argument name="firstName" type="string" defaultValue=""/> + <argument name="lastName" type="string" defaultValue=""/> + <argument name="street1" type="string" defaultValue=""/> + <argument name="state" type="string" defaultValue=""/> + <argument name="postcode" type="string" defaultValue=""/> + <argument name="country" type="string" defaultValue=""/> + <argument name="telephone" type="string" defaultValue=""/> + </arguments> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <see userInput="{{firstName}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="firstName"/> + <see userInput="{{lastName}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="lastName"/> + <see userInput="{{street1}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="street1"/> + <see userInput="{{state}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="state"/> + <see userInput="{{postcode}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="postcode"/> + <see userInput="{{country}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="country"/> + <see userInput="{{telephone}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="telephone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultShippingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..9d7c209121fd6 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerDefaultShippingAddressActionGroup.xml @@ -0,0 +1,30 @@ +<?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"> + <!-- Assert Customer Default Shipping Address --> + <actionGroup name="AdminAssertCustomerDefaultShippingAddress" > + <arguments> + <argument name="firstName" type="string" defaultValue=""/> + <argument name="lastName" type="string" defaultValue=""/> + <argument name="street1" type="string" defaultValue=""/> + <argument name="state" type="string" defaultValue="" /> + <argument name="postcode" type="string" defaultValue=""/> + <argument name="country" type="string" defaultValue=""/> + <argument name="telephone" type="string" defaultValue=""/> + </arguments> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <see userInput="{{firstName}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="firstName"/> + <see userInput="{{lastName}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="lastName"/> + <see userInput="{{street1}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="street1"/> + <see userInput="{{state}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="state"/> + <see userInput="{{postcode}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="postcode"/> + <see userInput="{{country}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="country"/> + <see userInput="{{telephone}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="telephone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.xml new file mode 100644 index 0000000000000..d7529b3bdd58e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersGridActionGroup.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"> + <!--Assert customer info in customers grid row --> + <actionGroup name="AdminAssertCustomerInCustomersGrid"> + <arguments> + <argument name="text" type="string"/> + <argument name="row" type="string"/> + </arguments> + <see selector="{{AdminCustomerGridSection.gridRow(row)}}" userInput="{{text}}" stepKey="seeCustomerInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultBillingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultBillingAddressActionGroup.xml new file mode 100644 index 0000000000000..5557025c4b1de --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultBillingAddressActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Assert Customer Have No Default Billing Address --> + <actionGroup name="AdminAssertCustomerNoDefaultBillingAddress" > + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <see userInput="The customer does not have default billing address" selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="see"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultShippingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..e33ebbb96ee19 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerNoDefaultShippingAddressActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Assert Customer Have No Default Shipping Address --> + <actionGroup name="AdminAssertCustomerNoDefaultShippingAddress" > + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <see userInput="The customer does not have default shipping address" selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="see"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInCustomersAddressGridActionGroup.xml new file mode 100644 index 0000000000000..390f723d91f17 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInCustomersAddressGridActionGroup.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"> + <!--Assert number of records in customer address grid --> + <actionGroup name="AdminAssertNumberOfRecordsInCustomersAddressGrid"> + <arguments> + <argument name="number" type="string"/> + </arguments> + <see userInput="{{number}} records found" selector="{{AdminCustomerAddressesGridActionsSection.headerRow}}" stepKey="seeRecords"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml index 4c458c66ca65b..37149e23dc87e 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml @@ -42,4 +42,26 @@ <click stepKey="saveAddress" selector="{{AdminCustomerAddressesSection.saveAddress}}"/> <waitForPageLoad stepKey="waitForAddressSave"/> </actionGroup> + + <actionGroup name="AdminCreateCustomerWithWebSiteAndGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_Customer"/> + <argument name="website" type="string" defaultValue="customWebsite"/> + <argument name="storeView" type="string" defaultValue="customStore"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersPage"/> + <click stepKey="addNewCustomer" selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}"/> + <selectOption stepKey="selectWebSite" selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{website}}"/> + <click selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="ClickToExpandGroup"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceGroupOrCatalogOption('Default (General)')}}" stepKey="waitForCustomerGroupExpand"/> + <click selector="{{AdminCustomerAccountInformationSection.groupValue('Default (General)')}}" after="waitForCustomerGroupExpand" stepKey="ClickToSelectGroup"/> + <fillField stepKey="FillFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{customerData.firstname}}"/> + <fillField stepKey="FillLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{customerData.lastname}}"/> + <fillField stepKey="FillEmail" selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{customerData.email}}"/> + <selectOption stepKey="selectStoreView" selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{storeView}}"/> + <waitForElement selector="{{AdminCustomerAccountInformationSection.storeView}}" stepKey="waitForCustomerStoreViewExpand"/> + <click stepKey="save" selector="{{AdminCustomerAccountInformationSection.saveCustomer}}"/> + <waitForPageLoad stepKey="waitForCustomersPage"/> + <see stepKey="seeSuccessMessage" userInput="You saved the customer."/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml new file mode 100644 index 0000000000000..86039056999b0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.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="AdminFilterCustomerByEmail"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerIndexPage"/> + <waitForPageLoad stepKey="waitToCustomerIndexPageToLoad"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerIndexPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <fillField userInput="{{email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml new file mode 100644 index 0000000000000..03b950a6dbe6f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml @@ -0,0 +1,14 @@ +<?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"> + <!-- Save Customer and Assert Success Message --> + <actionGroup name="AdminCustomerSaveAndContinue" > + <click selector="{{AdminCustomerMainActionsSection.saveAndContinue}}" stepKey="saveAndContinue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml new file mode 100644 index 0000000000000..f5d5682e374f2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.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="AdminAddProductToShoppingCartActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.productItem}}" stepKey="waitForElementVisible"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.productItem}}" stepKey="expandProductItem"/> + <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.productNameFilter}}" stepKey="waitForProductFilterFieldVisible"/> + <fillField selector="{{AdminCustomerShoppingCartProductItemSection.productNameFilter}}" stepKey="setProductName" userInput="{{productName}}"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForAjaxLoad stepKey="waitForAjax"/> + <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.firstProductCheckbox}}" stepKey="waitForElementCheckboxVisible"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.firstProductCheckbox}}" stepKey="selectFirstCheckbox"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.addSelectionsToMyCartButton}}" stepKey="clickAddSelectionsToMyCartButton" after="selectFirstCheckbox"/> + <waitForAjaxLoad stepKey="waitForAjax2"/> + <seeElement stepKey="seeAddedProduct" selector="{{AdminCustomerShoppingCartProductItemSection.addedProductName('productName')}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml new file mode 100644 index 0000000000000..b61b353714e19 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.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"> + <!--Delete customer address from grid row, row starts at 0 --> + <actionGroup name="AdminDeleteAddressInCustomersAddressGrid"> + <arguments> + <argument name="row" type="string"/> + </arguments> + <click selector="{{AdminCustomerAddressesGridSection.checkboxByRow(row)}}" stepKey="clickRowCustomerAddressCheckbox"/> + <click selector="{{AdminCustomerAddressesGridSection.selectLinkByRow(row)}}" stepKey="openActionsDropdown"/> + <click selector="{{AdminCustomerAddressesGridSection.deleteLinkByRow(row)}}" stepKey="chooseDeleteOption"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> + <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml index 2609f0ab5c0d6..788e5f8967f43 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml @@ -18,8 +18,8 @@ <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> <fillField userInput="{{customerGroupName}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('customer_group_code')}}" stepKey="fillNameFieldOnFiltersSection"/> <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> - <click selector="{{AdminCustomerGroupGridActionsSection.selectButton('customerGroupName')}}" stepKey="clickSelectButton"/> - <click selector="{{AdminCustomerGroupGridActionsSection.deleteAction('customerGroupName')}}" stepKey="clickOnDeleteItem"/> + <click selector="{{AdminCustomerGroupGridActionsSection.selectButton(customerGroupName)}}" stepKey="clickSelectButton"/> + <click selector="{{AdminCustomerGroupGridActionsSection.deleteAction(customerGroupName)}}" stepKey="clickOnDeleteItem"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDeleteCustomerGroup"/> <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressNoZipNoStateActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressNoZipNoStateActionGroup.xml new file mode 100644 index 0000000000000..954b83bead1d3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressNoZipNoStateActionGroup.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="AdminEditCustomerAddressNoZipNoState" extends="AdminEditCustomerAddressesFrom"> + <remove keyForRemoval="selectState"/> + <remove keyForRemoval="fillZipCode"/> + <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> + <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressSetDefaultShippingAndBillingActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressSetDefaultShippingAndBillingActionGroup.xml new file mode 100644 index 0000000000000..0c1af1cb5b67c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressSetDefaultShippingAndBillingActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AdminEditCustomerAddressSetDefaultShippingAndBilling" extends="AdminEditCustomerAddressesFrom"> + <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> + <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml new file mode 100644 index 0000000000000..594337c1a6922 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml @@ -0,0 +1,42 @@ +<?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"> + <!-- Same as "EditCustomerAddressesFromAdminActionGroup" but taking country and state from input "customerAddress" --> + <actionGroup name="AdminEditCustomerAddressesFrom" > + <arguments> + <argument name="customerAddress" type="entity"/> + </arguments> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="addNewAddresses"/> + <waitForPageLoad time="60" stepKey="wait5678" /> + <fillField stepKey="fillPrefixName" userInput="{{customerAddress.prefix}}" selector="{{AdminEditCustomerAddressesSection.prefixName}}"/> + <fillField stepKey="fillMiddleName" userInput="{{customerAddress.middlename}}" selector="{{AdminEditCustomerAddressesSection.middleName}}"/> + <fillField stepKey="fillSuffixName" userInput="{{customerAddress.suffix}}" selector="{{AdminEditCustomerAddressesSection.suffixName}}"/> + <fillField stepKey="fillCompany" userInput="{{customerAddress.company}}" selector="{{AdminEditCustomerAddressesSection.company}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{customerAddress.street[0]}}" selector="{{AdminEditCustomerAddressesSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{customerAddress.city}}" selector="{{AdminEditCustomerAddressesSection.city}}"/> + <selectOption stepKey="selectCountry" selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{customerAddress.country_id}}"/> + <selectOption stepKey="selectState" selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{customerAddress.state}}"/> + <fillField stepKey="fillZipCode" userInput="{{customerAddress.postcode}}" selector="{{AdminEditCustomerAddressesSection.zipCode}}"/> + <fillField stepKey="fillPhone" userInput="{{customerAddress.telephone}}" selector="{{AdminEditCustomerAddressesSection.phone}}"/> + <fillField stepKey="fillVAT" userInput="{{customerAddress.vat_id}}" selector="{{AdminEditCustomerAddressesSection.vat}}"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + </actionGroup> + <actionGroup name="AdminEditCustomerAddressSetDefaultShippingAndBilling" extends="AdminEditCustomerAddressesFrom"> + <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> + <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> + </actionGroup> + <actionGroup name="AdminEditCustomerAddressNoZipNoState" extends="AdminEditCustomerAddressesFrom"> + <remove keyForRemoval="selectState"/> + <remove keyForRemoval="fillZipCode"/> + <click selector="{{AdminEditCustomerAddressesSection.defaultBillingAddressButton}}" stepKey="setDefaultBilling" before="setDefaultShipping"/> + <click selector="{{AdminEditCustomerAddressesSection.defaultShippingAddressButton}}" stepKey="setDefaultShipping" before="fillPrefixName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerInformationFromActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerInformationFromActionGroup.xml new file mode 100644 index 0000000000000..ddeefeb3c3742 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerInformationFromActionGroup.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"> + <!-- Edit Customer Account Information Required Fields in Admin --> + <actionGroup name="AdminEditCustomerAccountInformationActionGroup" > + <arguments> + <argument name="firstName" type="string"/> + <argument name="lastName" type="string"/> + <argument name="email" type="string"/> + </arguments> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="goToAccountInformation"/> + <clearField stepKey="clearFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}"/> + <fillField stepKey="fillFirstName" userInput="{{firstName}}" selector="{{AdminCustomerAccountInformationSection.firstName}}"/> + <clearField stepKey="clearLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}"/> + <fillField stepKey="fillLastName" userInput="{{lastName}}" selector="{{AdminCustomerAccountInformationSection.lastName}}"/> + <clearField stepKey="clearEmail" selector="{{AdminCustomerAccountInformationSection.email}}"/> + <fillField stepKey="fillEmail" userInput="{{email}}" selector="{{AdminCustomerAccountInformationSection.email}}"/> + <click selector="{{AdminCustomerMainActionsSection.saveAndContinue}}" stepKey="saveAndContinue"/> + <waitForPageLoad stepKey="wait"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerAddressGridByPhoneNumberActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerAddressGridByPhoneNumberActionGroup.xml new file mode 100644 index 0000000000000..2d0d44a4cc529 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerAddressGridByPhoneNumberActionGroup.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"> + <!--Filter customer address grid by phone number --> + <actionGroup name="AdminFilterCustomerAddressGridByPhoneNumber"> + <arguments> + <argument name="phone" type="string"/> + </arguments> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="openFilters"/> + <fillField selector="{{AdminCustomerAddressFiltersSection.telephoneInput}}" userInput="{{phone}}" stepKey="fill"/> + <click selector="{{AdminCustomerAddressFiltersSection.applyFilter}}" stepKey="clickApplyFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerByNameActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerByNameActionGroup.xml new file mode 100644 index 0000000000000..c49a0dbe20ae7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerByNameActionGroup.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="AdminFilterCustomerByName"> + <arguments> + <argument name="customerName" type="string"/> + </arguments> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <fillField userInput="{{customerName}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('name')}}" stepKey="fillNameFieldOnFiltersSection"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGridByEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGridByEmailActionGroup.xml new file mode 100644 index 0000000000000..9cab8a790ff58 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGridByEmailActionGroup.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"> + <!--Filter customer grid by the email --> + <actionGroup name="AdminFilterCustomerGridByEmail"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilters"/> + <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{email}}" stepKey="fillEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="clickApplyFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml new file mode 100644 index 0000000000000..8e6b56b19d674 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.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="AdminOpenCustomerEditPageActionGroup"> + <arguments> + <argument name="customerId" type="string" /> + </arguments> + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="openCustomerEditPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml new file mode 100644 index 0000000000000..135f010784199 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.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"> + <!--Reset customers grid filter and to default view --> + <actionGroup name="AdminResetFilterInCustomerAddressGrid"> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminCustomerAddressFiltersSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> + <click selector="{{AdminCustomerAddressFiltersSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> + <waitForPageLoad stepKey="waitForGridLoad"/> + <see selector="{{AdminCustomerFiltersSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerGridActionGroup.xml new file mode 100644 index 0000000000000..5c6ff347d565a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerGridActionGroup.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"> + <!--Reset customers grid filter and to default view --> + <actionGroup name="AdminResetFilterInCustomerGrid"> + <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminCustomerFiltersSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> + <click selector="{{AdminCustomerFiltersSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> + <waitForPageLoad stepKey="waitForGridLoad"/> + <see selector="{{AdminCustomerFiltersSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAndAssertSuccessMessageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAndAssertSuccessMessageActionGroup.xml new file mode 100644 index 0000000000000..d3907e96b0d77 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSaveCustomerAndAssertSuccessMessageActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Save Customer and Assert Success Message --> + <actionGroup name="AdminSaveCustomerAndAssertSuccessMessage" > + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <see userInput="You saved the customer" selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="seeMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectAllCustomersActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectAllCustomersActionGroup.xml new file mode 100644 index 0000000000000..1a8b4da67e74a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectAllCustomersActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AdminSelectAllCustomers"> + <checkOption selector="{{AdminCustomerGridMainActionsSection.multicheck}}" stepKey="checkAllCustomers"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.xml new file mode 100644 index 0000000000000..bb84d578fd9ed --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.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="AdminSelectCustomerByEmail"> + <arguments> + <argument name="customerEmail" type="string"/> + </arguments> + <checkOption selector="{{AdminCustomerGridSection.customerCheckboxByEmail(customerEmail)}}" stepKey="checkCustomerBox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml new file mode 100644 index 0000000000000..b1b82fb9fb74c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml @@ -0,0 +1,38 @@ +<?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="AdminUpdateCustomerGroupByEmailActionGroup"> + <arguments> + <argument name="emailAddress"/> + <argument name="customerGroup" type="string"/> + </arguments> + + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomerPage01"/> + + <!-- Start of Action Group: searchAdminDataGridByKeyword --> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters0"/> + <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="{{emailAddress}}" stepKey="fillKeywordSearchField01"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickKeywordSearch01"/> + <waitForPageLoad stepKey="waitForPageLoad02"/> + <!-- End of Action Group: searchAdminDataGridByKeyword --> + + <click selector="{{AdminGridRow.editByValue(emailAddress)}}" stepKey="clickOnCustomer01"/> + <waitForPageLoad stepKey="waitForPageLoad03"/> + + <conditionalClick selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" dependentSelector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" visible="true" stepKey="clickOnAccountInformation01"/> + <waitForPageLoad stepKey="waitForPageLoad04"/> + + <click selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="clickOnCustomerGroup01"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup01"/> + + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickOnSave01"/> + <waitForPageLoad stepKey="waitForPageLoad05"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml new file mode 100644 index 0000000000000..186d0244e8c71 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup"> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.password}}" stepKey="waitPasswordFieldVisible"/> + <assertElementContainsAttribute selector="{{StorefrontCustomerSignInPopupFormSection.password}}" attribute="autocomplete" expectedValue="off" stepKey="assertAuthorizationPopupPasswordAutocompleteOff"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml new file mode 100644 index 0000000000000..132b5ca81886f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.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="AssertCustomerAccountPageTitleActionGroup"> + <arguments> + <argument name="pageTitle" type="string" /> + </arguments> + <see selector="{{StorefrontCustomerAccountMainSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="assertPageTitle" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml new file mode 100644 index 0000000000000..26c4f23fc9a77 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.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="AssertCustomerGroupNotInGridActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> + <fillField userInput="{{customerGroup.code}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('customer_group_code')}}" stepKey="fillNameFieldOnFiltersSection"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml new file mode 100644 index 0000000000000..94e01db5c1ff8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.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="AssertCustomerGroupNotOnProductFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> + <grabMultiple selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelectOptions('0')}}" stepKey="customerGroups" /> + <assertNotContains stepKey="assertCustomerGroupNotInOptions"> + <actualResult type="variable">customerGroups</actualResult> + <expectedResult type="string">{{customerGroup.code}}</expectedResult> + </assertNotContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml new file mode 100644 index 0000000000000..2c8e0081e5e90 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.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="AssertCustomerGroupOnCustomerFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="clickOnAccountInfoTab" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <seeOptionIsSelected userInput="{{customerGroup.code}}" selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="verifyNeededCustomerGroupSelected" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml new file mode 100644 index 0000000000000..d9da950fe7115 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.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="AssertCustomerWelcomeMessageActionGroup"> + <arguments> + <argument name="customerFullName" type="string" /> + </arguments> + <see userInput="Welcome, {{customerFullName}}!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml new file mode 100644 index 0000000000000..644254443d129 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.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="AssertCustomerResetPasswordActionGroup"> + <arguments> + <argument name="url" type="string"/> + <argument name="message" type="string" defaultValue="" /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + + <waitForElementVisible selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}" stepKey="waitForMessage" /> + <see stepKey="seeMessage" userInput="{{message}}" selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}"/> + <seeInCurrentUrl stepKey="seeCorrectCurrentUrl" url="{{url}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml new file mode 100644 index 0000000000000..ab48184ed98a8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.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="AssertMessageCustomerChangeAccountInfoActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="You saved the account information." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerAccountMainSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml new file mode 100644 index 0000000000000..65c9b025a9c2d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.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="AssertMessageCustomerCreateAccountActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Thank you for registering with Main Website Store." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerAccountMainSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml new file mode 100644 index 0000000000000..6b88661985873 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.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="AssertMessageCustomerLoginActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later." /> + <argument name="messageType" type="string" defaultValue="error" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml new file mode 100644 index 0000000000000..23a067cd94eea --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AssertStorefrontPasswordAutoCompleteOffActionGroup"> + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <assertElementContainsAttribute selector="{{StorefrontCustomerSignInFormSection.passwordField}}" attribute="autocomplete" expectedValue="off" stepKey="assertSignInPasswordAutocompleteOff"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml similarity index 93% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml index a68042127ec48..047f656f5eabe 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CreateCustomerActionGroup"> <click stepKey="openCustomers" selector="{{AdminMenuSection.customers}}"/> <waitForAjaxLoad stepKey="waitForCatalogSubmenu" time="5"/> @@ -39,6 +40,5 @@ <click stepKey="save" selector="{{NewCustomerPageSection.saveCustomer}}"/> <waitForPageLoad stepKey="waitForCustomersPage" time="10"/> <waitForElementVisible selector="{{NewCustomerPageSection.createdSuccessMessage}}" stepKey="waitForSuccessfullyCreatedMessage" time="20"/> - </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml new file mode 100644 index 0000000000000..06659dae156a4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml @@ -0,0 +1,46 @@ +<?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="DeleteCustomerActionGroup"> + <arguments> + <argument name="lastName" defaultValue=""/> + </arguments> + <!--Clear filter if exist--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingCustomerFilters"/> + + <click stepKey="chooseCustomer" selector="{{CustomersPageSection.customerCheckbox(lastName)}}"/> + <waitForAjaxLoad stepKey="waitForThick" time="2"/> + <click stepKey="OpenActions" selector="{{CustomersPageSection.actions}}"/> + <waitForAjaxLoad stepKey="waitForDelete" time="5"/> + <click stepKey="ChooseDelete" selector="{{CustomersPageSection.delete}}"/> + <waitForPageLoad stepKey="waitForDeleteItemPopup" time="10"/> + <click stepKey="clickOnOk" selector="{{CustomersPageSection.ok}}"/> + <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="10"/> + </actionGroup> + <actionGroup name="DeleteCustomerByEmailActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForAdminCustomerPageLoad"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <waitForPageLoad stepKey="waitForClearFilters"/> + <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{email}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCustomerGridSection.selectFirstRow}}" stepKey="clickOnEditButton1"/> + <click selector="{{CustomersPageSection.actions}}" stepKey="clickActionsDropdown"/> + <click selector="{{CustomersPageSection.delete}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{CustomersPageSection.ok}}" stepKey="waitForOkToVisible"/> + <click selector="{{CustomersPageSection.ok}}" stepKey="clickOkConfirmationButton"/> + <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="30"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml new file mode 100644 index 0000000000000..617c895bc1201 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.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="EditCustomerAddressesFromAdminActionGroup" > + <arguments> + <argument name="customerAddress"/> + </arguments> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="addNewAddresses"/> + <waitForPageLoad time="60" stepKey="wait5678" /> + <fillField stepKey="fillPrefixName" userInput="{{customerAddress.prefix}}" selector="{{AdminEditCustomerAddressesSection.prefixName}}"/> + <fillField stepKey="fillMiddleName" userInput="{{customerAddress.middlename}}" selector="{{AdminEditCustomerAddressesSection.middleName}}"/> + <fillField stepKey="fillSuffixName" userInput="{{customerAddress.suffix}}" selector="{{AdminEditCustomerAddressesSection.suffixName}}"/> + <fillField stepKey="fillCompany" userInput="{{customerAddress.company}}" selector="{{AdminEditCustomerAddressesSection.company}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{customerAddress.street}}" selector="{{AdminEditCustomerAddressesSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{customerAddress.city}}" selector="{{AdminEditCustomerAddressesSection.city}}"/> + <selectOption stepKey="selectCountry" selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{US_Address_CA.country_id}}"/> + <selectOption stepKey="selectState" selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{US_Address_CA.state}}"/> + <fillField stepKey="fillZipCode" userInput="{{customerAddress.postcode}}" selector="{{AdminEditCustomerAddressesSection.zipCode}}"/> + <fillField stepKey="fillPhone" userInput="{{customerAddress.telephone}}" selector="{{AdminEditCustomerAddressesSection.phone}}"/> + <fillField stepKey="fillVAT" userInput="{{customerAddress.vat_id}}" selector="{{AdminEditCustomerAddressesSection.vat}}"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml index 7be36ffbd9bc4..703b9f542f81a 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml @@ -11,9 +11,12 @@ <arguments> <argument name="Customer"/> </arguments> - <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> - <fillField stepKey="fillEmail" userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> - <fillField stepKey="fillPassword" userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> - <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <waitForPageLoad time="30" stepKey="waitPageFullyLoaded"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="waitFormAppears"/> + <fillField userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <waitForPageLoad stepKey="waitForCustomerLoggedIn" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml new file mode 100644 index 0000000000000..071450001051e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.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="LoginToStorefrontWithEmailAndPassword"> + <arguments> + <argument name="email" type="string"/> + <argument name="password" type="string"/> + </arguments> + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <waitForPageLoad stepKey="wait"/> + <fillField stepKey="fillEmail" userInput="{{email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml new file mode 100644 index 0000000000000..be639d245f022 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml @@ -0,0 +1,15 @@ +<?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="NavigateToAllCustomerPage"> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml new file mode 100644 index 0000000000000..076797f349107 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml @@ -0,0 +1,15 @@ +<?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="NavigateToCustomerGroupPage"> + <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="openCustomersGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml new file mode 100644 index 0000000000000..5591bee529690 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.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="NavigateThroughCustomerTabsActionGroup"> + <arguments> + <argument name="navigationItemName" type="string" /> + </arguments> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(navigationItemName)}}" stepKey="clickOnDesiredNavItem" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml index af918e8208566..208f4f51e38e6 100755 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml @@ -12,13 +12,14 @@ <argument name="customer"/> </arguments> <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{customer.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEdit"/> - <waitForPageLoad stepKey="waitForPageLoad2" /> + <waitForPageLoad stepKey="waitForPageLoad3"/> </actionGroup> <actionGroup name="OpenEditCustomerAddressFromAdminActionGroup"> <arguments> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml new file mode 100644 index 0000000000000..6ca0f612deeaa --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="OpenMyAccountPageActionGroup"> + <click selector="{{LoggedInCustomerHeaderLinksSection.customerDropdownMenu}}" stepKey="openCustomerDropdownMenu"/> + <click selector="{{LoggedInCustomerHeaderLinksSection.myAccount}}" stepKey="clickOnMyAccount"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml new file mode 100644 index 0000000000000..ca5e16c4ddb40 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.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="SetCustomerGroupForSelectedCustomersViaGrid"> + <arguments> + <argument name="groupName" type="string"/> + </arguments> + <click selector="{{CustomersPageSection.actions}}" stepKey="clickActions"/> + <click selector="{{CustomersPageSection.actionItem('Assign a Customer Group')}}" stepKey="clickAssignAction"/> + <executeJS function="document.getElementsByClassName('action-menu _active')[0].scrollBy(0, 10000)" stepKey="scrollToGroup"/> + <click selector="{{CustomersPageSection.assignGroup(groupName)}}" stepKey="selectGroup"/> + <waitForPageLoad stepKey="waitAfterSelectingGroup"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml index 76acf6e865963..ef956293d367b 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml @@ -24,7 +24,35 @@ <see stepKey="seeLastName" userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> <see stepKey="seeEmail" userInput="{{Customer.email}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> </actionGroup> - + + <actionGroup name="StorefrontCreateCustomerSignedUpNewsletterActionGroup"> + <arguments> + <argument name="customer" defaultValue="CustomerEntityOne"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForNavigateToCustomersPageLoad"/> + <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> + <fillField stepKey="fillFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> + <fillField stepKey="fillLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> + <checkOption selector="{{StorefrontCustomerCreateFormSection.signUpForNewsletter}}" stepKey="checkSignUpForNewsletter"/> + <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> + <fillField stepKey="fillConfirmPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> + <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}"/> + <waitForPageLoad stepKey="waitForCreateAccountButtonToLoad" /> + </actionGroup> + <actionGroup name="AssertSignedUpNewsletterActionGroup"> + <arguments> + <argument name="customer" defaultValue="CustomerEntityOne"/> + <argument name="storeName" defaultValue="Main Website" type="string"/> + </arguments> + <see stepKey="successMessage" userInput="Thank you for registering with {{storeName}} Store." selector="{{AdminCustomerMessagesSection.successMessage}}"/> + <see stepKey="seeFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <see stepKey="seeLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <see stepKey="seeEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" /> + <seeInCurrentUrl url="{{StorefrontCustomerDashboardPage.url}}" stepKey="seeAssertInCurrentUrl"/> + </actionGroup> + <actionGroup name="EnterCustomerAddressInfo"> <arguments> <argument name="Address"/> @@ -39,15 +67,91 @@ <fillField stepKey="fillStreetAddress1" selector="{{StorefrontCustomerAddressSection.streetAddress1}}" userInput="{{Address.street[0]}}"/> <fillField stepKey="fillStreetAddress2" selector="{{StorefrontCustomerAddressSection.streetAddress2}}" userInput="{{Address.street[1]}}"/> <fillField stepKey="fillCityName" selector="{{StorefrontCustomerAddressSection.city}}" userInput="{{Address.city}}"/> + <selectOption stepKey="selectCounty" selector="{{StorefrontCustomerAddressSection.country}}" userInput="{{Address.country_id}}"/> <selectOption stepKey="selectState" selector="{{StorefrontCustomerAddressSection.stateProvince}}" userInput="{{Address.state}}"/> <fillField stepKey="fillZip" selector="{{StorefrontCustomerAddressSection.zip}}" userInput="{{Address.postcode}}"/> - <selectOption stepKey="selectCounty" selector="{{StorefrontCustomerAddressSection.country}}" userInput="{{Address.country_id}}"/> - <click stepKey="saveAddress" selector="{{StorefrontCustomerAddressSection.saveAddress}}"/> </actionGroup> + <!-- Fills State Field instead of selecting it--> + <actionGroup name="EnterCustomerAddressInfoFillState" extends="EnterCustomerAddressInfo"> + <fillField stepKey="selectState" selector="{{StorefrontCustomerAddressSection.stateProvinceFill}}" userInput="{{Address.state}}"/> + </actionGroup> + + <actionGroup name="VerifyCustomerBillingAddress"> + <arguments> + <argument name="address"/> + </arguments> + <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> + <waitForPageLoad stepKey="waitForAddressPageLoad"/> + <!--Verify customer default billing address--> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultBillingAddressFirstnameAndLastname"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultBillingAddressCompany"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet1"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.city}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultBillingAddressCityAndPostcode"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultBillingAddressCountry"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultBillingAddressTelephone"/> + </actionGroup> + <actionGroup name="VerifyCustomerShippingAddress"> + <arguments> + <argument name="address"/> + </arguments> + <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> + <waitForPageLoad stepKey="waitForAddressPageLoad"/> + <!--Verify customer default shipping address--> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultShippingAddressFirstnameAndLastname"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultShippingAddressCompany"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet1"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.city}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultShippingAddressCityAndPostcode"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultShippingAddressTelephone"/> + </actionGroup> + <actionGroup name="VerifyCustomerBillingAddressWithState"> + <arguments> + <argument name="address"/> + </arguments> + <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> + <waitForPageLoad stepKey="waitForAddressPageLoad"/> + <!--Verify customer default billing address--> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultBillingAddressFirstnameAndLastname"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultBillingAddressCompany"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultBillingAddressStreet1"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.city}}, {{address.state}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultBillingAddressCityAndPostcode"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultBillingAddressCountry"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultBillingAddressTelephone"/> + </actionGroup> + <actionGroup name="VerifyCustomerShippingAddressWithState"> + <arguments> + <argument name="address"/> + </arguments> + <amOnPage url="customer/address/index/" stepKey="goToAddressPage"/> + <waitForPageLoad stepKey="waitForAddressPageLoad"/> + <!--Verify customer default shipping address--> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.firstname}} {{address.lastname}}" stepKey="seeAssertCustomerDefaultShippingAddressFirstnameAndLastname"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.company}}" stepKey="seeAssertCustomerDefaultShippingAddressCompany"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[0]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.street[1]}}" stepKey="seeAssertCustomerDefaultShippingAddressStreet1"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.city}}, {{address.state}}, {{address.postcode}}" stepKey="seeAssertCustomerDefaultShippingAddressCityAndPostcode"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.country}}" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{address.telephone}}" stepKey="seeAssertCustomerDefaultShippingAddressTelephone"/> + </actionGroup> + <actionGroup name="VerifyCustomerNameOnFrontend"> + <arguments> + <argument name="customer"/> + </arguments> + <!--Verify customer name on frontend--> + <amOnPage url="customer/account/edit/" stepKey="goToAddressPage"/> + <waitForPageLoad stepKey="waitForAddressPageLoad"/> + <click selector="{{StorefrontCustomerSidebarSection.sidebarCurrentTab('Account Information')}}" stepKey="clickAccountInformationFromSidebarCurrentTab"/> + <waitForPageLoad stepKey="waitForAccountInformationTabToOpen"/> + <seeInField selector="{{StorefrontCustomerAccountInformationSection.firstName}}" userInput="{{customer.firstname}}" stepKey="seeAssertCustomerFirstName"/> + <seeInField selector="{{StorefrontCustomerAccountInformationSection.lastName}}" userInput="{{customer.lastname}}" stepKey="seeAssertCustomerLastName"/> + </actionGroup> <actionGroup name="SignUpNewCustomerStorefrontActionGroup" extends="SignUpNewUserFromStorefrontActionGroup"> <waitForPageLoad stepKey="waitForRegistered" after="clickCreateAccountButton"/> - <remove keyForRemoval="seeThankYouMessage" after="waitForRegistered"/> + <remove keyForRemoval="seeThankYouMessage"/> </actionGroup> -</actionGroups> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml new file mode 100644 index 0000000000000..a45fcf31f7b3f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml @@ -0,0 +1,48 @@ +<?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="StorefrontAddNewCustomerAddressActionGroup"> + <amOnPage url="customer/address/new/" stepKey="OpenCustomerAddNewAddress"/> + <arguments> + <argument name="Address"/> + </arguments> + <fillField stepKey="fillFirstName" userInput="{{Address.firstname}}" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="{{Address.lastname}}" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <fillField stepKey="fillCompanyName" userInput="{{Address.company}}" selector="{{StorefrontCustomerAddressFormSection.company}}"/> + <fillField stepKey="fillPhoneNumber" userInput="{{Address.telephone}}" selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{Address.street[0]}}" selector="{{StorefrontCustomerAddressFormSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{Address.city}}" selector="{{StorefrontCustomerAddressFormSection.city}}"/> + <selectOption stepKey="selectState" userInput="{{Address.state}}" selector="{{StorefrontCustomerAddressFormSection.state}}"/> + <fillField stepKey="fillZip" userInput="{{Address.postcode}}" selector="{{StorefrontCustomerAddressFormSection.zip}}"/> + <selectOption stepKey="selectCountry" userInput="{{Address.country}}" selector="{{StorefrontCustomerAddressFormSection.country}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + </actionGroup> + <actionGroup name="StorefrontAddCustomerDefaultAddressActionGroup"> + <amOnPage url="customer/address/new/" stepKey="OpenCustomerAddNewAddress"/> + <arguments> + <argument name="Address"/> + </arguments> + <fillField stepKey="fillFirstName" userInput="{{Address.firstname}}" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="{{Address.lastname}}" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <fillField stepKey="fillCompanyName" userInput="{{Address.company}}" selector="{{StorefrontCustomerAddressFormSection.company}}"/> + <fillField stepKey="fillPhoneNumber" userInput="{{Address.telephone}}" selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{Address.street[0]}}" selector="{{StorefrontCustomerAddressFormSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{Address.city}}" selector="{{StorefrontCustomerAddressFormSection.city}}"/> + <selectOption stepKey="selectState" userInput="{{Address.state}}" selector="{{StorefrontCustomerAddressFormSection.state}}"/> + <fillField stepKey="fillZip" userInput="{{Address.postcode}}" selector="{{StorefrontCustomerAddressFormSection.zip}}"/> + <selectOption stepKey="selectCountry" userInput="{{Address.country}}" selector="{{StorefrontCustomerAddressFormSection.country}}"/> + <click stepKey="checkUseAsDefaultBillingAddressCheckBox" selector="{{StorefrontCustomerAddressFormSection.useAsDefaultBillingAddressCheckBox}}"/> + <click stepKey="checkUseAsDefaultShippingAddressCheckBox" selector="{{StorefrontCustomerAddressFormSection.useAsDefaultShippingAddressCheckBox}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml new file mode 100644 index 0000000000000..475702ad69221 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.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="StorefrontAssertSuccessLoginToStorefront" extends="LoginToStorefrontActionGroup"> + <arguments> + <argument name="Customer" type="entity"/> + </arguments> + <see stepKey="assertWelcome" userInput="{{Customer.firstname}}" selector="{{StorefrontPanelHeaderSection.customerWelcome}}" after="clickSignInAccountButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml new file mode 100644 index 0000000000000..dfb9d1b2c259a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup"> + <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" /> + <waitForPageLoad stepKey="waitForCustomerSaved" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml new file mode 100644 index 0000000000000..b12858fc1037e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontClickSignInButtonActionGroup"> + <click stepKey="signIn" selector="{{StorefrontPanelHeaderSection.customerLoginLink}}" /> + <waitForPageLoad stepKey="waitForStorefrontSignInPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml new file mode 100644 index 0000000000000..9cd52b841fca4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontClickSignOnCustomerLoginFormActionGroup"> + <click stepKey="clickSignInButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" /> + <waitForPageLoad stepKey="waitForCustomerSignIn" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml index fc5c1b881752e..c3b92b1af7f82 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml @@ -9,8 +9,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CustomerLogoutStorefrontByMenuItemsActionGroup"> - <conditionalClick selector="{{StorefrontPanelHeaderSection.customerWelcome}}" - dependentSelector="{{StorefrontPanelHeaderSection.customerWelcomeMenu}}" + <conditionalClick selector="{{StorefrontPanelHeaderSection.customerWelcomeMenu}}" + dependentSelector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" visible="false" stepKey="clickHeaderCustomerMenuButton" /> <click selector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" stepKey="clickSignOutButton" /> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookContainsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookContainsActionGroup.xml new file mode 100644 index 0000000000000..8385dc17ecf98 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookContainsActionGroup.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"> + <!-- Go to Address Book --> + <actionGroup name="StorefrontCustomerAddressBookContains"> + <arguments> + <argument name="text" type="string"/> + </arguments> + <see userInput="{{text}}" selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="containsText"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNotContainsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNotContainsActionGroup.xml new file mode 100644 index 0000000000000..afef2d9a04e34 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNotContainsActionGroup.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"> + <!-- Go to Address Book --> + <actionGroup name="StorefrontCustomerAddressBookNotContains"> + <arguments> + <argument name="text" type="string"/> + </arguments> + <dontSee userInput="{{text}}" selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="doesNotContainsText"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.xml new file mode 100644 index 0000000000000..febc482d62e8b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.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"> + <!-- Go to Address Book --> + <actionGroup name="StorefrontCustomerAddressBookNumberOfAddresses"> + <arguments> + <argument name="number" type="string"/> + </arguments> + <see userInput="{{number}} Item" selector="{{StorefrontCustomerAddressesSection.numberOfAddresses}}" stepKey="checkNumberOfAddresses"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml new file mode 100644 index 0000000000000..844d13aa1fe43 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontCustomerChangeEmailActionGroup"> + <arguments> + <argument name="email" type="string" /> + <argument name="password" type="string" /> + </arguments> + <conditionalClick selector="{{StorefrontCustomerSidebarSection.sidebarTab('Account Information')}}" dependentSelector="{{StorefrontCustomerSidebarSection.sidebarTab('Account Information')}}" visible="true" stepKey="openAccountInfoTab" /> + <waitForPageLoad stepKey="waitForAccountInfoTabOpened" /> + <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckbox" /> + <fillField selector="{{StorefrontCustomerAccountInformationSection.email}}" userInput="{{email}}" stepKey="fillEmail" /> + <fillField selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}" userInput="{{password}}" stepKey="fillCurrentPassword" /> + <click selector="{{StorefrontCustomerAccountInformationSection.saveButton}}" stepKey="saveChange" /> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml new file mode 100644 index 0000000000000..84d2f353b51d2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.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"> + <!-- Go to Address Book --> + <actionGroup name="StorefrontCustomerGoToSidebarMenu"> + <arguments> + <argument name="menu" type="string"/> + </arguments> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(menu)}}" stepKey="goToAddressBook"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml new file mode 100644 index 0000000000000..a28593f1b77b7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml @@ -0,0 +1,24 @@ +<?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="StorefrontCustomerResetPasswordActionGroup"> + <arguments> + <argument name="email" type="string" /> + </arguments> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <click stepKey="clickForgotPasswordLink" selector="{{StorefrontCustomerSignInFormSection.forgotPasswordLink}}"/> + <see stepKey="seePageTitle" userInput="Forgot Your Password" selector="{{StorefrontForgotPasswordSection.pageTitle}}"/> + <!-- Enter email and submit the forgot password form --> + <fillField stepKey="fillEmailField" userInput="{{email}}" selector="{{StorefrontForgotPasswordSection.email}}"/> + <click stepKey="clickResetPassword" selector="{{StorefrontForgotPasswordSection.resetMyPasswordButton}}"/> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml new file mode 100644 index 0000000000000..62d77d4548cce --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.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="StorefrontFillCustomerAccountCreationFormActionGroup"> + <arguments> + <argument name="customer" type="entity" /> + </arguments> + + <fillField stepKey="fillFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}" /> + <fillField stepKey="fillLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}" /> + <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> + <fillField stepKey="fillConfirmPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml new file mode 100644 index 0000000000000..22883ada7c2b1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.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="StorefrontFillCustomerLoginFormActionGroup"> + <arguments> + <argument name="customer" type="entity" /> + </arguments> + + <fillField userInput="{{customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="{{customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml new file mode 100644 index 0000000000000..0ae470d0497ab --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontOpenCustomerAccountCreatePageActionGroup"> + <amOnPage url="{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml new file mode 100644 index 0000000000000..c1ea2da8a9519 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontOpenCustomerAccountInfoEditPageActionGroup"> + <amOnPage url="{{StorefrontCustomerEditPage.url}}" stepKey="goToCustomerEditPage"/> + <waitForPageLoad stepKey="waitForEditPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml new file mode 100644 index 0000000000000..0a5c72265528a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml @@ -0,0 +1,15 @@ +<?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="StorefrontOpenCustomerLoginPageActionGroup"> + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml similarity index 84% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml index 7c774a634b369..4c59edbcb8057 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Sign out--> <actionGroup name="SignOut"> <click selector="{{SignOutSection.admin}}" stepKey="clickToAdminProfile"/> @@ -24,5 +24,4 @@ <fillField userInput="{{NewAdmin.password}}" selector="{{LoginFormSection.password}}" stepKey="fillPassword"/> <click selector="{{LoginFormSection.signIn}}" stepKey="clickLogin"/> </actionGroup> - -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml new file mode 100644 index 0000000000000..712d3a59a2144 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.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="VerifyCustomerGroupForCustomer"> + <arguments> + <argument name="customerEmail" type="string"/> + <argument name="groupName" type="string"/> + </arguments> + <click selector="{{AdminCustomerGridSection.customerEditLinkByEmail(customerEmail)}}" stepKey="openCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPage"/> + <see userInput="{{groupName}}" selector="{{AdminEditCustomerInformationSection.group}}" stepKey="checkCustomerGroup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index c7335f9024218..5d1a900167144 100755 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -44,12 +44,29 @@ <data key="city">Austin</data> <data key="state">Texas</data> <data key="country_id">US</data> + <data key="country">United States</data> <data key="postcode">78729</data> <data key="telephone">512-345-6789</data> <data key="default_billing">Yes</data> <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionTX</requiredEntity> </entity> + <entity name="US_Address_TX_Default_Billing" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <data key="default_billing">Yes</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> <entity name="US_Address_NY" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -68,6 +85,23 @@ <requiredEntity type="region">RegionNY</requiredEntity> <data key="country">United States</data> </entity> + <entity name="US_Address_NY_Default_Shipping" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">368</data> + <array key="street"> + <item>368 Broadway St.</item> + <item>113</item> + </array> + <data key="city">New York</data> + <data key="state">New York</data> + <data key="country_id">US</data> + <data key="postcode">10001</data> + <data key="telephone">512-345-6789</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionNY</requiredEntity> + <data key="country">United States</data> + </entity> <entity name="US_Address_NY_Not_Default_Address" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -95,6 +129,7 @@ <data key="city">Los Angeles</data> <data key="state">California</data> <data key="country_id">US</data> + <data key="country">United States</data> <data key="postcode">90001</data> <data key="telephone">512-345-6789</data> <data key="default_billing">Yes</data> @@ -148,4 +183,92 @@ </array> <data key="state">California</data> </entity> + <entity name="US_Default_Billing_Address_TX" type="address" extends="US_Address_TX"> + <data key="default_billing">false</data> + <data key="default_shipping">true</data> + </entity> + <entity name="US_Default_Shipping_Address_CA" type="address" extends="US_Address_CA"> + <data key="default_billing">true</data> + <data key="default_shipping">false</data> + </entity> + <entity name="addressNoZipNoState" type="address"> + <data key="country_id">United Kingdom</data> + <array key="street"> + <item>3962 Horner Street</item> + </array> + <data key="company">Magento</data> + <data key="telephone">334-200-4061</data> + <data key="city">London</data> + <data key="firstname">Fn</data> + <data key="lastname">Ln</data> + <data key="middlename">Mn</data> + <data key="prefix">Mr</data> + <data key="suffix">Sr</data> + <data key="vat_id">U1234567891</data> + <data key="default_shipping">true</data> + <data key="default_billing">true</data> + </entity> + <entity name="updateCustomerUKAddress" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <data key="telephone">0123456789-02134567</data> + <array key="street"> + <item>172, Westminster Bridge Rd</item> + <item>7700 xyz street</item> + </array> + <data key="country_id">GB</data> + <data key="country">United Kingdom</data> + <data key="city">London</data> + <!-- State not required for UK address on frontend--> + <data key="state"> </data> + <data key="postcode">12345</data> + </entity> + <entity name="updateCustomerFranceAddress" type="address"> + <data key="firstname">Jaen</data> + <data key="lastname">Reno</data> + <data key="company">Magento</data> + <data key="telephone">555-888-111-999</data> + <array key="street"> + <item>18-20 Rue Maréchal Lecler</item> + <item>18-20 Rue Maréchal Lecler</item> + </array> + <data key="country_id">FR</data> + <data key="country">France</data> + <data key="city">Quintin</data> + <data key="state">Côtes-d'Armor</data> + <data key="postcode">12345</data> + </entity> + <entity name="updateCustomerNoXSSInjection" type="address"> + <data key="firstname">Jany</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <data key="telephone">555-888-111-999</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + <item>7700 West Parmer Lane</item> + </array> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="city">Denver</data> + <data key="state">Colorado</data> + <data key="postcode">12345</data> + </entity> + <entity name="PolandAddress" type="address"> + <data key="firstname">Mag</data> + <data key="lastname">Ento</data> + <data key="company">Magento</data> + <array key="street"> + <item>Piwowarska 6</item> + </array> + <data key="city">Bielsko-Biała</data> + <data key="state"> Bielsko</data> + <data key="country_id">PL</data> + <data key="country">Poland</data> + <data key="postcode">43-310</data> + <data key="telephone">799885616</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionUT</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..4e433c76b3ddb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?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="AdminMenuCustomers"> + <data key="pageTitle">Customers</data> + <data key="title">Customers</data> + <data key="dataUiId">magento-customer-customer</data> + </entity> + <entity name="AdminMenuCustomersAllCustomers"> + <data key="pageTitle">Customers</data> + <data key="title">All Customers</data> + <data key="dataUiId">magento-customer-customer-manage</data> + </entity> + <entity name="AdminMenuCustomersCustomerGroups"> + <data key="pageTitle">Customer Groups</data> + <data key="title">Customer Groups</data> + <data key="dataUiId">magento-customer-customer-group</data> + </entity> + <entity name="AdminMenuCustomersNowOnline"> + <data key="pageTitle">Customers Now Online</data> + <data key="title">Now Online</data> + <data key="dataUiId">magento-customer-customer-online</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml index 3cbd70d342824..11a47459ab7b3 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml @@ -21,4 +21,11 @@ <entity name="GlobalCustomerAccountSharing" type="account_share_scope_value"> <data key="value">0</data> </entity> + + <entity name="CustomerAccountSharingSystemValue" type="customer_account_sharing_config_inherit"> + <requiredEntity type="account_share_scope_inherit">CustomerAccountSharingInherit</requiredEntity> + </entity> + <entity name="CustomerAccountSharingInherit" type="account_share_scope_inherit"> + <data key="inherit">true</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index 44ac0981d9577..f561e413a01f1 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -21,6 +21,7 @@ <data key="firstname">John</data> <data key="lastname">Doe</data> <data key="middlename">S</data> + <data key="fullname">John Doe</data> <data key="password">pwdTest123!</data> <data key="prefix">Mr</data> <data key="suffix">Sr</data> @@ -45,6 +46,42 @@ <data key="website_id">0</data> <requiredEntity type="address">US_Address_TX</requiredEntity> </entity> + <entity name="UsCustomerAssignedToNewCustomerGroup" type="customer"> + <var key="group_id" entityKey="id" entityType="customerGroup" /> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX</requiredEntity> + </entity> + <entity name="Simple_US_Customer_Incorrect_Name" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">LoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsum</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX</requiredEntity> + </entity> + <entity name="Simple_Customer_Without_Address" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> <entity name="Simple_US_Customer_Multiple_Addresses" type="customer"> <data key="group_id">0</data> <data key="default_billing">true</data> @@ -73,6 +110,20 @@ <requiredEntity type="address">US_Address_NY_Not_Default_Address</requiredEntity> <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> </entity> + <entity name="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX_Default_Billing</requiredEntity> + <requiredEntity type="address">US_Address_NY_Default_Shipping</requiredEntity> + </entity> <entity name="Simple_US_Customer_NY" type="customer"> <data key="group_id">0</data> <data key="default_billing">true</data> @@ -142,4 +193,27 @@ <data key="website_id">0</data> <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> </entity> + <entity name="Customer_With_Different_Default_Billing_Shipping_Addresses" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Default_Billing_Address_TX</requiredEntity> + <requiredEntity type="address">US_Default_Shipping_Address_CA</requiredEntity> + </entity> + <entity name="Colorado_US_Customer" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">Patric.Patric@example.com</data> + <data key="firstname">Patrick</title></head><svg/onload=alert('XSS')></data> + <data key="lastname"><script>alert('Last name')</script></data> + <data key="password">123123^q</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml index c1f11c9e9c390..28305d37cf77b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -8,14 +8,30 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="NotLoggedInCustomerGroup" type="customerGroup"> + <data key="id">0</data> + <data key="code">NOT LOGGED IN</data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> <entity name="GeneralCustomerGroup" type="customerGroup"> <data key="code">General</data> <data key="tax_class_id">3</data> <data key="tax_class_name">Retail Customer</data> </entity> + <entity name="CustomerGroupChange" type="customerGroup"> + <data key="code" unique="suffix">Group_</data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> <entity name="DefaultCustomerGroup" type="customerGroup"> <array key="group_names"> <item>General</item> </array> </entity> + <entity name="CustomCustomerGroup" type="customerGroup"> + <data key="code" unique="suffix">Group </data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml new file mode 100644 index 0000000000000..347a04251f9cd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml @@ -0,0 +1,49 @@ +<?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="NewCustomerData" type="braintree_config_state"> + <data key="FirstName">Abgar</data> + <data key="LastName">Abgaryan</data> + <data key="Email">m@m.com</data> + <data key="AddressFirstName">Abgar</data> + <data key="AddressLastName">Abgaryan</data> + <data key="StreetAddress">Street</data> + <data key="City">Yerevan</data> + <data key="Zip">9999</data> + <data key="PhoneNumber">9999</data> + <data key="Country">Armenia</data> + </entity> + <entity name="Simple_UK_Customer_For_Shipment" type="customer"> + <data key="firstName">John</data> + <data key="lastName">Doe</data> + <data key="email">johndoe@example.com</data> + <data key="company">Test Company</data> + <data key="streetFirstLine">39 St Maurices Road</data> + <data key="streetSecondLine">ap. 654</data> + <data key="city">PULDAGON</data> + <data key="telephone">077 5866 0667</data> + <data key="country">United Kingdom</data> + <data key="region"> </data> + <data key="postcode">KW1 7NQ</data> + </entity> + <entity name="Simple_US_CA_Customer_For_Shipment" type="customer"> + <data key="firstName">John</data> + <data key="lastName">Doe</data> + <data key="email">johndoeusca@example.com</data> + <data key="company">Magento</data> + <data key="streetFirstLine">123 Alphabet Drive</data> + <data key="streetSecondLine">ap. 350</data> + <data key="city">Los Angeles</data> + <data key="telephone">555-55-555-55</data> + <data key="country">United States</data> + <data key="region">California</data> + <data key="postcode">90240</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml index 41701bfac11ad..c3132b5b6a44f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml @@ -18,4 +18,16 @@ </object> </object> </operation> + <operation name="CustomerAccountShareConfigInherit" dataType="customer_account_sharing_config_inherit" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" + method="POST"> + <object key="groups" dataType="customer_account_sharing_config_inherit"> + <object key="account_share" dataType="customer_account_sharing_config_inherit"> + <object key="fields" dataType="customer_account_sharing_config_inherit"> + <object key="scope" dataType="account_share_scope_inherit"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> </operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml new file mode 100644 index 0000000000000..3139ea278a0dd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomerGroup" dataType="customerGroup" type="create" auth="adminOauth" url="/V1/customerGroups" method="POST"> + <contentType>application/json</contentType> + <object dataType="customerGroup" key="group"> + <field key="code">string</field> + <field key="tax_class_id">integer</field> + <field key="tax_class_name">string</field> + </object> + </operation> + <operation name="DeleteCustomerGroup" dataType="customerGroup" type="delete" auth="adminOauth" url="/V1/customerGroups/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml index d662c5cef6032..9bd382da8eb92 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml @@ -13,5 +13,6 @@ <section name="AdminCustomerAddressesGridActionsSection"/> <section name="AdminCustomerAddressesSection"/> <section name="AdminCustomerMainActionsSection"/> + <section name="AdminEditCustomerAddressesSection" /> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml new file mode 100644 index 0000000000000..43198297b1731 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAccountChangePasswordPage" url="/customer/account/edit/changepass/1/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAccountInformationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml index e2ebf638934c6..0d273da353005 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml @@ -9,6 +9,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontCustomerCreatePage" url="/customer/account/create/" area="storefront" module="Magento_Customer"> - <section name="StorefrontCustomerCreateFormSection" /> + <section name="StorefrontCustomerCreateFormSection"/> + <section name="StoreFrontCustomerAdvancedAttributesSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml new file mode 100644 index 0000000000000..d4cf90dde08ff --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerEditPage" url="/customer/account/edit/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAccountInformationSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml index 0d4fef8f6e967..b4814a3e4bedd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontCustomerSignInPage" url="/customer/account/login/" area="storefront" module="Magento_Customer"> <section name="StorefrontCustomerSignInFormSection" /> + <section name="StorefrontCustomerLoginMessagesSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml new file mode 100644 index 0000000000000..2633a0c760cec --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontForgotPasswordPage" url="/customer/account/forgotpassword/" area="storefront" module="Magento_Customer"> + <section name="StorefrontForgotPasswordSection" /> + </page> +</pages> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml similarity index 81% rename from app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml rename to app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml index 98d748b5a30ea..376b0b9f66db9 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreateUserSection"> <element name="system" type="input" selector="#menu-magento-backend-system"/> <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> @@ -20,4 +22,4 @@ <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> <element name="saveButton" type="button" selector="#save"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml index d5a410164a6f1..71e3e673477d2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerAccountInformationSection"> + <element name="accountInformationTab" type="button" selector="#tab_customer" timeout="30"/> <element name="statusInactive" type="button" selector=".admin__actions-switch-label"/> <element name="accountInformationTitle" type="text" selector=".admin__page-nav-title"/> <element name="accountInformationButton" type="text" selector="//a/span[text()='Account Information']"/> @@ -17,10 +18,18 @@ <element name="lastName" type="input" selector="input[name='customer[lastname]']"/> <element name="email" type="input" selector="input[name='customer[email]']"/> <element name="group" type="select" selector="[name='customer[group_id]']"/> + <element name="groupIdValue" type="text" selector="//*[@name='customer[group_id]']/option"/> <element name="groupValue" type="button" selector="//span[text()='{{groupValue}}']" parameterized="true"/> <element name="associateToWebsite" type="select" selector="//select[@name='customer[website_id]']"/> <element name="saveCustomer" type="button" selector="//button[@title='Save Customer']"/> <element name="saveCustomerAndContinueEdit" type="button" selector="//button[@title='Save and Continue Edit']"/> <element name="storeView" type="select" selector="//select[@name='customer[sendemail_store_id]']"/> + <element name="namePrefix" type="input" selector="//input[contains(@name, 'customer[prefix]')]"/> + <element name="nameSuffix" type="input" selector="//input[contains(@name, 'customer[suffix]')]"/> + <element name="dateOfBirth" type="input" selector="//input[contains(@name, 'customer[dob]')]"/> + <element name="gender" type="select" selector="//select[contains(@name, 'customer[gender]')]"/> + <element name="firstNameRequiredMessage" type="text" selector="//input[@name='customer[firstname]']/../label[contains(.,'This is a required field.')]"/> + <element name="lastNameRequiredMessage" type="text" selector="//input[@name='customer[lastname]']/../label[contains(.,'This is a required field.')]"/> + <element name="emailRequiredMessage" type="text" selector="//input[@name='customer[email]']/../label[contains(.,'This is a required field.')]"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml index b9a3839ff9894..f3df6cc5e8c00 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml @@ -20,5 +20,7 @@ <element name="telephoneInput" type="input" selector="input[name=telephone]"/> <element name="applyFilter" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> <element name="clearAll" type="button" selector=".admin__data-grid-header .action-tertiary.action-clear" timeout="30"/> + <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> + <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml index fb153a7c102a5..e639fca834b2b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml @@ -12,6 +12,5 @@ <element name="customerGrid" type="text" selector="table[data-role='grid']"/> <element name="firstRowSelectActionLink" type="text" selector="tr[data-repeat-index='0'] .action-select" timeout="30"/> <element name="firstRowEditActionLink" type="text" selector="tr[data-repeat-index='0'] [data-action='item-edit']" timeout="30"/> - </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml index d8d93814333ca..e743c4af66d9f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml @@ -12,9 +12,10 @@ <element name="spinner" type="button" selector=".spinner"/> <element name="gridLoadingMask" type="button" selector=".admin__data-grid-loading-mask"/> <element name="search" type="input" selector="#fulltext"/> - <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> + <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']" timeout="30"/> <element name="actions" type="text" selector="//div[@class='admin__data-grid-header']//button[@class='action-select']"/> <element name="filters" type="button" selector="button[data-action='grid-filter-expand']" timeout="30"/> - <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="30"/> + <element name="headerRow" type="text" selector=".admin__data-grid-header-row.row.row-gutter"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml index 85c086d01848b..5393d6c1ab9b9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml @@ -19,5 +19,8 @@ <element name="secondRowCheckbox" type="checkbox" selector="//tr[contains(@data-repeat-index, '1')]//input[contains(@data-action, 'select-row')]"/> <element name="checkboxByName" type="checkbox" selector="//div[contains(text(),'{{customer}}')]/ancestor::tr[contains(@class, 'data-row')]//input[@class='admin__control-checkbox']" parameterized="true" /> <element name="rowsInGrid" type="text" selector="//tr[contains(@class,'data-row')]"/> + <element name="checkboxByRow" type="checkbox" selector="//tr[contains(@data-repeat-index, '{{row}}')]//input[contains(@data-action, 'select-row')]" parameterized="true"/> + <element name="selectLinkByRow" type="text" selector="//tr[contains(@data-repeat-index, '{{row}}')]//button[@class='action-select']" parameterized="true"/> + <element name="deleteLinkByRow" type="text" selector="//tr[contains(@data-repeat-index, '{{row}}')]//a[contains(@data-action,'item-delete')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml index 8068f94032730..26df107708c47 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml @@ -30,5 +30,10 @@ <element name="customerAddressRow" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true"/> <element name="deleteButton" type="button" selector="//button[@id='delete']"/> <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="streetRequiredMessage" type="text" selector="//input[@name='street[0]']/../label[contains(.,'This is a required field.')]"/> + <element name="cityRequiredMessage" type="text" selector="//input[@name='city']/../label[contains(.,'This is a required field.')]"/> + <element name="countryRequiredMessage" type="text" selector="//select[@name='country_id']/../label[contains(.,'This is a required field.')]"/> + <element name="postcodeRequiredMessage" type="text" selector="//input[@name='postcode']/../label[contains(.,'This is a required field.')]"/> + <element name="phoneNumberRequiredMessage" type="text" selector="//input[@name='telephone']/../label[contains(.,'This is a required field.')]"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml index 9e104eb52cf90..a934d71397b8c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -8,5 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerConfigSection"> <element name="customerDataLifetime" type="input" selector="#customer_online_customers_section_data_lifetime"/> + <element name="accountSharingOptionsTab" type="button" selector="#customer_account_share-head"/> + <element name="shareCustomerAccountInherit" type="checkbox" selector="#customer_account_share_scope_inherit"/> + <element name="shareCustomerAccount" type="select" selector="#customer_account_share_scope"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml index 02d9bc2eb5f12..17a4a283c2648 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml @@ -16,5 +16,8 @@ <element name="apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> <element name="clearAllFilters" type="text" selector=".admin__current-filters-actions-wrap.action-clear"/> <element name="clearAll" type="button" selector=".admin__data-grid-header .action-tertiary.action-clear" timeout="30"/> + <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> + <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> + <element name="countryOptions" type="button" selector=".admin__data-grid-filters select[name=billing_country_id] option"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml index d9d3bfe7f737c..91363c614c1f8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridSection.xml @@ -11,5 +11,10 @@ <section name="AdminCustomerGridSection"> <element name="customerGrid" type="text" selector="table[data-role='grid']"/> <element name="firstRowEditLink" type="text" selector="tr[data-repeat-index='0'] .action-menu-item" timeout="30"/> + <element name="gridRow" type="text" selector="//*[@data-role='sticky-el-root']/parent::div/parent::div/following-sibling::div//tbody//*[@class='data-row'][{{row}}]" parameterized="true"/> + <element name="selectFirstRow" type="checkbox" selector="//td[@class='data-grid-checkbox-cell']"/> + <element name="customerCheckboxByEmail" type="checkbox" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//input[@type='checkbox']" parameterized="true" timeout="30"/> + <element name="customerEditLinkByEmail" type="text" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//a[@class='action-menu-item']" parameterized="true" timeout="30"/> + <element name="customerGroupByEmail" type="text" selector="//tr[@class='data-row' and //div[text()='{{customerEmail}}']]//div[text()='{{customerGroup}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml index 1fdb15f189ace..4cb7f5e3f628e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml @@ -15,5 +15,6 @@ <element name="selectFirstRow" type="button" selector="//button[@class='action-select']"/> <element name="deleteBtn" type="button" selector="//*[text()='Delete']"/> <element name="clearAllBtn" type="button" selector="//button[text()='Clear all']"/> + <element name="editButtonByCustomerGroupCode" type="button" selector="//tr[.//td[count(//th[./*[.='Group']]/preceding-sibling::th) + 1][./*[.='{{code}}']]]//a[contains(@href, '/edit/')]" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml index 3ff880c64e6d6..304068d89b729 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml @@ -10,6 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerMainActionsSection"> <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> <element name="resetPassword" type="button" selector="#resetPassword" timeout="30"/> + <element name="manageShoppingCart" type="button" selector="#manage_quote" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml new file mode 100644 index 0000000000000..c4a4d650c1e59 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerShoppingCartSection"> + <element name="createOrderButton" type="button" selector="button[title='Create Order']"/> + </section> + + <section name="AdminCustomerShoppingCartProductItemSection"> + <element name="productItem" type="button" selector="#dt-products"/> + <element name="productNameFilter" type="input" selector="#source_products_filter_name"/> + <element name="searchButton" type="button" selector="//*[@id='anchor-content']//button[@title='Search']"/> + <element name="firstProductCheckbox" type="checkbox" selector="//*[@id='source_products_table']/tbody/tr[1]//*[@name='source_products']"/> + <element name="addSelectionsToMyCartButton" type="button" selector="//*[@id='products_search']/div[1]//*[text()='Add selections to my cart']"/> + <element name="addedProductName" type="text" selector="//*[@id='order-items_grid']//*[text()='{{var}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml new file mode 100644 index 0000000000000..0ba197999be6c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminDeleteUserSection"> + <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> + <element name="password" selector="#user_current_password" type="input"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml new file mode 100644 index 0000000000000..ffddc6292ef5d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml @@ -0,0 +1,34 @@ +<?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="AdminEditCustomerAddressesSection"> + <element name="addresses" type="button" selector="//span[text()='Addresses']" timeout="30"/> + <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> + <element name="defaultBillingAddress" type="text" selector="input[name='default_billing']"/> + <element name="defaultShippingAddress" type="text" selector="input[name='default_shipping']"/> + <element name="defaultBillingAddressButton" type="text" selector="//input[@name='default_billing']/following-sibling::label"/> + <element name="defaultShippingAddressButton" type="text" selector="//input[@name='default_shipping']/following-sibling::label"/> + <element name="prefixName" type="text" selector="input[name='prefix']"/> + <element name="firstName" type="text" selector="input[name='firstname']" /> + <element name="middleName" type="text" selector="input[name='middlename']" /> + <element name="lastName" type="text" selector="input[name='lastname']" /> + <element name="suffixName" type="text" selector="input[name='suffix']" /> + <element name="company" type="text" selector="input[name='company']" /> + <element name="streetAddress" type="text" selector="input[name='street[0]']" /> + <element name="city" type="text" selector="//*[@class='modal-component']//input[@name='city']" /> + <element name="country" type="select" selector="//*[@class='modal-component']//select[@name='country_id']" /> + <element name="state" type="select" selector="//*[@class='modal-component']//select[@name='region_id']" /> + <element name="zipCode" type="text" selector="//*[@class='modal-component']//input[@name='postcode']" /> + <element name="phone" type="text" selector="//*[@class='modal-component']//input[@name='telephone']" /> + <element name="vat" type="text" selector="input[name='vat_id']" /> + <element name="save" type="button" selector="//button[@title='Save']" /> + </section> + +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml index f5bbb84eaa593..1c5bbc76e4d6e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml @@ -11,5 +11,8 @@ <section name="AdminEditCustomerInformationSection"> <element name="orders" type="button" selector="#tab_orders_content" timeout="30"/> <element name="addresses" type="button" selector="//a[@id='tab_address']" timeout="30"/> + <element name="newsLetter" type="button" selector="//a[@class='admin__page-nav-link' and @id='tab_newsletter_content']" timeout="30"/> + <element name="group" type="text" selector="//th[text()='Customer Group:']/../td"/> + <element name="customerTitle" type="text" selector="h1.page-title"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerNewsletterSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerNewsletterSection.xml new file mode 100644 index 0000000000000..51b4b54c5c8b6 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerNewsletterSection.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="AdminEditCustomerNewsletterSection"> + <element name="subscribedToNewsletter" type="checkbox" selector="//div[@class='admin__field-control control']/input[@name='subscription']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml new file mode 100644 index 0000000000000..7c4a76871d58c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml @@ -0,0 +1,19 @@ +<?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="AdminUserGridSection"> + <element name="usernameFilterTextField" type="input" selector="#permissionsUserGrid_filter_username"/> + <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> + <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> + <element name="usernameInFirstRow" type="text" selector=".col-username"/> + <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> + <element name="successMessage" type="text" selector=".message-success"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml new file mode 100644 index 0000000000000..93a988caf3d1c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml @@ -0,0 +1,21 @@ +<?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="CustomersPageSection"> + <element name="addNewCustomerButton" type="button" selector="//*[@id='add']"/> + <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> + <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']" timeout="30"/> + <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']" timeout="30"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="30"/> + <element name="actionItem" type="button" selector="//div[@class='admin__data-grid-outer-wrap']/div[@class='admin__data-grid-header']//span[text()='{{actionItem}}']" parameterized="true" timeout="30"/> + <element name="assignGroup" type="button" selector="//div[@class='admin__data-grid-outer-wrap']/div[@class='admin__data-grid-header']//ul[@class='action-submenu _active']//span[text()='{{groupName}}']" parameterized="true"/> + <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml new file mode 100644 index 0000000000000..6eeef1ba9daf0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.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="CustomersSubmenuSection"> + <element name="allCustomers" type="button" selector="//li[@id='menu-magento-customer-customer']//li[@data-ui-id='menu-magento-customer-customer-manage']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml new file mode 100644 index 0000000000000..907551e932fcf --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="LoggedInCustomerHeaderLinksSection"> + <element name="customerDropdownMenu" type="button" selector=".customer-name"/> + <element name="myAccount" type="button" selector="//*[contains(@class, 'page-header')]//*[contains(@class, 'customer-menu')]//a[contains(., 'My Account')]"/> + <element name="myWishList" type="button" selector=".page-header .customer-menu .wishlist"/> + <element name="signOut" type="button" selector=".page-header .customer-menu .authorization-link"/> + </section> +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml similarity index 92% rename from app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml rename to app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml index d302f9c7d0cba..abb8aa6c1d826 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewCustomerPageSection"> <element name="associateToWebsite" type="select" selector="//*[@class='admin__field-control _with-tooltip']//*[@class='admin__control-select']"/> <element name="group" type="select" selector="//div[@class='admin__field-control admin__control-fields required']//div[@class='admin__field-control']//select[@class='admin__control-select']"/> @@ -28,6 +28,5 @@ <element name="phoneNumber" type="input" selector="//input[contains(@name, 'telephone')]"/> <element name="saveCustomer" type="button" selector="//button[@title='Save Customer']"/> <element name="createdSuccessMessage" type="button" selector="//div[@data-ui-id='messages-message-success']"/> - </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..8a633ec5bc271 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,24 @@ +<?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="StorefrontCustomerAccountInformationSection"> + <element name="firstName" type="input" selector="#firstname"/> + <element name="lastName" type="input" selector="#lastname"/> + <element name="changeEmail" type="checkbox" selector=".form-edit-account input[name='change_email']"/> + <element name="changePassword" type="checkbox" selector=".form-edit-account input[name='change_password']"/> + <element name="testAddedAttributeFiled" type="input" selector="//input[contains(@id,'{{var}}')]" parameterized="true"/> + <element name="saveButton" type="button" selector="#form-validate .action.save.primary" timeout="30"/> + <element name="currentPassword" type="input" selector="#current-password"/> + <element name="newPassword" type="input" selector="#password"/> + <element name="confirmNewPassword" type="input" selector="#password-confirmation"/> + <element name="confirmNewPasswordError" type="text" selector="#password-confirmation-error"/> + <element name="email" type="input" selector=".form-edit-account input[name='email']" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml new file mode 100644 index 0000000000000..c00b54b3964da --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.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="StorefrontCustomerAccountMainSection"> + <element name="pageTitle" type="text" selector="#maincontent .column.main [data-ui-id='page-title-wrapper']" /> + <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml new file mode 100644 index 0000000000000..112ced1bc375f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml @@ -0,0 +1,25 @@ +<?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="StorefrontCustomerAddressFormSection"> + <element name="firstName" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'firstname')]"/> + <element name="lastName" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'lastname')]"/> + <element name="company" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'company')]"/> + <element name="phoneNumber" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'telephone')]"/> + <element name="streetAddress" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'street')]"/> + <element name="city" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'city')]"/> + <element name="state" type="select" selector="//form[@class='form-address-edit']//select[contains(@name, 'region_id')]"/> + <element name="zip" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'postcode')]"/> + <element name="country" type="select" selector="//form[@class='form-address-edit']//select[contains(@name, 'country_id')]"/> + <element name="useAsDefaultBillingAddressCheckBox" type="input" selector="//form[@class='form-address-edit']//input[@name='default_billing']"/> + <element name="useAsDefaultShippingAddressCheckBox" type="input" selector="//form[@class='form-address-edit']//input[@name='default_shipping']"/> + <element name="saveAddress" type="button" selector="//button[@title='Save Address']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml index 05bbc559defac..aad9d02842271 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml @@ -9,7 +9,14 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerAddressesSection"> - <element name="addressesList" type="text" selector=".block-addresses-list" /> - <element name="deleteAdditionalAddress" type="button" selector="//ol[@class='items addresses']/li[@class='item'][{{var}}]//a[@class='action delete']" parameterized="true"/> + <element name="defaultBillingAddress" type="text" selector=".box-address-billing" /> + <element name="editDefaultBillingAddress" type="text" selector="//div[@class='box-actions']//span[text()='Change Billing Address']" timeout="30"/> + <element name="defaultShippingAddress" type="text" selector=".box-address-shipping" /> + <element name="editDefaultShippingAddress" type="text" selector="//div[@class='box-actions']//span[text()='Change Shipping Address']" timeout="30"/> + <element name="addressesList" type="text" selector=".additional-addresses" /> + <element name="deleteAdditionalAddress" type="button" selector="//tbody//tr[{{var}}]//a[@class='action delete']" parameterized="true"/> + <element name="editAdditionalAddress" type="button" selector="//tbody//tr[{{var}}]//a[@class='action edit']" parameterized="true" timeout="30"/> + <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> + <element name="numberOfAddresses" type="text" selector=".toolbar-number"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml index 2b5662cdd623e..8881a2a012ce8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -11,9 +11,24 @@ <section name="StorefrontCustomerCreateFormSection"> <element name="firstnameField" type="input" selector="#firstname"/> <element name="lastnameField" type="input" selector="#lastname"/> + <element name="lastnameLabel" type="text" selector="//label[@for='lastname']"/> + <element name="signUpForNewsletter" type="checkbox" selector="//div/input[@name='is_subscribed']"/> <element name="emailField" type="input" selector="#email_address"/> <element name="passwordField" type="input" selector="#password"/> <element name="confirmPasswordField" type="input" selector="#password-confirmation"/> <element name="createAccountButton" type="button" selector="button.action.submit.primary" timeout="30"/> </section> + <section name="StoreFrontCustomerAdvancedAttributesSection"> + <element name="textFieldAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true" /> + <element name="textAreaAttribute" type="input" selector="//textarea[@id='{{var}}']" parameterized="true" /> + <element name="multiLineFirstAttribute" type="input" selector="//input[@id='{{var}}_0']" parameterized="true" /> + <element name="multiLineSecondAttribute" type="input" selector="//input[@id='{{var}}_1']" parameterized="true" /> + <element name="datedAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true" /> + <element name="dropDownAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true" /> + <element name="dropDownOptionAttribute" type="text" selector="//*[@id='{{var}}']/option[2]" parameterized="true" /> + <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[3]" parameterized="true" /> + <element name="yesNoAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true" /> + <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"/> + </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml index 70d1bb6675db5..93e7bf71b0894 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml @@ -10,6 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerDashboardAccountInformationSection"> <element name="ContactInformation" type="textarea" selector=".box.box-information .box-content"/> + <element name="edit" type="block" selector=".action.edit" timeout="15"/> + <element name="changePassword" type="block" selector=".action.change-password" timeout="15"/> </section> <section name="StorefrontCustomerAddressSection"> <element name="firstName" type="input" selector="#firstname"/> @@ -20,6 +22,7 @@ <element name="streetAddress2" type="input" selector="#street_2"/> <element name="city" type="input" selector="#city"/> <element name="stateProvince" type="select" selector="#region_id"/> + <element name="stateProvinceFill" type="input" selector="#region"/> <element name="zip" type="input" selector="#zip"/> <element name="country" type="select" selector="#country"/> <element name="saveAddress" type="button" selector="[data-action='save-address']" timeout="30"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml new file mode 100644 index 0000000000000..a9859cf58751b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerLoginMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message-error"/> + <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml new file mode 100644 index 0000000000000..07d044921c8e5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.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="StorefrontCustomerMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message-error"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml index 7482193031091..407c6480e9dde 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerSidebarSection"> - <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{var1}}']" parameterized="true"/> + <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{tabName}}']" parameterized="true"/> + <element name="sidebarCurrentTab" type="text" selector="//div[@id='block-collapsible-nav']//*[contains(text(), '{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml index 25c07ca9cb3c9..7bc057b8be7b7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -12,6 +12,7 @@ <element name="emailField" type="input" selector="#email"/> <element name="passwordField" type="input" selector="#pass"/> <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> + <element name="forgotPasswordLink" type="button" selector=".action.remind" timeout="10"/> </section> <section name="StorefrontCustomerSignInPopupFormSection"> <element name="errorMessage" type="input" selector="[data-ui-id='checkout-cart-validationmessages-message-error']"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontForgotPasswordSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontForgotPasswordSection.xml new file mode 100644 index 0000000000000..bdae69c425db1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontForgotPasswordSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontForgotPasswordSection"> + <element name="pageTitle" type="text" selector=".page-title"/> + <element name="email" type="input" selector="#email_address"/> + <element name="resetMyPasswordButton" type="button" selector=".action.submit.primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml index 1955c6a417ba9..3610532c5356b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml @@ -9,11 +9,15 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontPanelHeaderSection"> + <!-- Element name="WelcomeMessage" Deprecated due to incorrect naming convention please use name="welcomeMessage" --> <element name="WelcomeMessage" type="text" selector=".greet.welcome span"/> - <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)" timeout="30"/> + + <element name="welcomeMessage" type="text" selector="header>.panel .greet.welcome" /> + <element name="createAnAccountLink" type="select" selector="//div[@class='panel wrapper']//li/a[contains(.,'Create an Account')]" timeout="30"/> <element name="notYouLink" type="button" selector=".greet.welcome span a"/> - <element name="customerWelcome" type="text" selector=".panel.header .customer-welcome"/> - <element name="customerWelcomeMenu" type="text" selector=".panel.header .customer-welcome .customer-menu"/> + <element name="customerWelcome" type="text" selector=".panel.header .greet.welcome"/> + <element name="customerWelcomeMenu" type="text" selector=".panel.header .customer-welcome .customer-name"/> + <element name="customerLoginLink" type="button" selector=".panel.header .header.links .authorization-link a" timeout="30"/> <element name="customerLogoutLink" type="text" selector=".panel.header .customer-welcome .customer-menu .authorization-link a" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml similarity index 78% rename from app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml rename to app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml index 3a07cbc6dd145..4442e317694ee 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml @@ -7,8 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="LoginFormSection"> <element name="username" type="input" selector="#username"/> <element name="password" type="input" selector="#login"/> @@ -19,6 +18,4 @@ <element name="admin" type="button" selector=".admin__action-dropdown-text"/> <element name="logout" type="button" selector="//*[contains(text(), 'Sign Out')]"/> </section> - </sections> - diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml new file mode 100644 index 0000000000000..36592ab38e91d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCustomerRetailerWithoutAddressTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, retailer without address"/> + <description value="Login as admin and create customer retailer without address"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5310"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter the customer From grid--> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad time="30" stepKey="waitToCustomerPageLoad"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="fillCustomerGroup"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <reloadPage stepKey="reloadPage"/> + + <!--Verify Customer in grid --> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForCustomerPageToLoad"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="Retailer" stepKey="assertGroup"/> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> + <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + + <!--Assert Customer Form --> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <see selector="{{AdminCustomerAccountInformationSection.groupIdValue}}" userInput="Retailer" stepKey="seeCustomerGroup1"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml new file mode 100644 index 0000000000000..cbc8b89d3f242 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCustomerWithCountryPolandTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, from Poland"/> + <description value="Login as admin and create customer with Poland address"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5311"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter the created customer From grid--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> + <argument name="email" value="$$createCustomer.email$$"/> + </actionGroup> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + + <!--Add the Address --> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> + <waitForPageLoad stepKey="waitForAddressPageToLoad"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForNewAddressPageToLoad"/> + <checkOption selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="EnableDefaultBillingAddress"/> + <fillField selector="{{AdminEditCustomerAddressesSection.streetAddress}}" userInput="{{PolandAddress.street}}" stepKey="fillStreetAddress"/> + <fillField selector="{{AdminEditCustomerAddressesSection.city}}" userInput="{{PolandAddress.city}}" stepKey="fillCity"/> + <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> + <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{PolandAddress.country}}" stepKey="fillCountry"/> + <fillField selector="{{AdminEditCustomerAddressesSection.zipCode}}" userInput="{{PolandAddress.postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{AdminEditCustomerAddressesSection.phone}}" userInput="{{PolandAddress.telephone}}" stepKey="fillPhoneNumber"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToBeSaved"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + + <!-- Assert Customer in grid --> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> + <argument name="email" value="$$createCustomer.email$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <see userInput="$$createCustomer.firstname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="$$createCustomer.lastname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> + <see userInput="$$createCustomer.email$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + <see userInput="{{PolandAddress.country}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCountry"/> + <see userInput="{{PolandAddress.postcode}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPostCode"/> + <see userInput="{{PolandAddress.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> + + <!--Assert Customer Form --> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="$$createCustomer.firstname$$" stepKey="seeCustomerFirstName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="$$createCustomer.lastname$$" stepKey="seeCustomerLastName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="$$createCustomer.email$$" stepKey="seeCustomerEmail"/> + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="clickOnAddressButton"/> + <waitForPageLoad stepKey="waitForAddressGridToLoad"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.firstname$$" stepKey="seeAFirstNameInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.lastname$$" stepKey="seeLastNameInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.street}}" stepKey="seeStreetInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.city}}" stepKey="seeLCityInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.country}}" stepKey="seeCountrynDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.postcode}}" stepKey="seePostCodeInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{PolandAddress.telephone}}" stepKey="seePhoneNumberInDefaultAddressSection"/> + + <!--Assert Customer Address Grid --> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.street}}" stepKey="seeStreetAddress"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.city}}" stepKey="seeCity"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.country}}" stepKey="seeCountry"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.postcode}}" stepKey="seePostCode"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{PolandAddress.telephone}}" stepKey="seePhoneNumber"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml new file mode 100644 index 0000000000000..43f2aa7f8de95 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml @@ -0,0 +1,95 @@ +<?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="AdminCreateCustomerWithCountryUSATest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, from USA"/> + <description value="Login as admin and create customer with USA address"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5309"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter the customer From grid--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> + <argument name="email" value="$$createCustomer.email$$"/> + </actionGroup> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + + <!-- Add the Address --> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> + <waitForPageLoad stepKey="waitForAddressPageToLoad"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForNewAddressPageToLoad"/> + <checkOption selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="EnableDefaultBillingAddress"/> + <fillField selector="{{AdminEditCustomerAddressesSection.streetAddress}}" userInput="{{US_Address_CA.street}}" stepKey="fillStreetAddress"/> + <fillField selector="{{AdminEditCustomerAddressesSection.city}}" userInput="{{US_Address_CA.city}}" stepKey="fillCity"/> + <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> + <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{US_Address_CA.country}}" stepKey="fillCountry"/> + <selectOption selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{US_Address_CA.state}}" stepKey="fillState"/> + <fillField selector="{{AdminEditCustomerAddressesSection.zipCode}}" userInput="{{US_Address_CA.postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{AdminEditCustomerAddressesSection.phone}}" userInput="{{US_Address_CA.telephone}}" stepKey="fillPhoneNumber"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToBeSaved"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + + <!-- Assert Customer in grid --> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> + <argument name="email" value="$$createCustomer.email$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <see userInput="$$createCustomer.firstname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="$$createCustomer.lastname$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> + <see userInput="$$createCustomer.email$$" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + <see userInput="{{US_Address_CA.state}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertState"/> + <see userInput="{{US_Address_CA.country}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCountry"/> + <see userInput="{{US_Address_CA.postcode}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPostCode"/> + <see userInput="{{US_Address_CA.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> + + <!--Assert Customer Form --> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="$$createCustomer.firstname$$" stepKey="seeCustomerFirstName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="$$createCustomer.lastname$$" stepKey="seeCustomerLastName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="$$createCustomer.email$$" stepKey="seeCustomerEmail"/> + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="clickOnAddressButton"/> + <waitForPageLoad stepKey="waitForAddressGridToLoad"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.firstname$$" stepKey="seeAFirstNameInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="$$createCustomer.lastname$$" stepKey="seeLastNameInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.street}}" stepKey="seeStreetInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.city}}" stepKey="seeLCityInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.country}}" stepKey="seeCountrynDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.postcode}}" stepKey="seePostCodeInDefaultAddressSection"/> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{US_Address_CA.telephone}}" stepKey="seePhoneNumberInDefaultAddressSection"/> + + <!--Assert Customer Address Grid --> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.street}}" stepKey="seeStreetAddress"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.city}}" stepKey="seeCity"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.country}}" stepKey="seeCountry"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.state}}" stepKey="seeState"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.postcode}}" stepKey="seePostCode"/> + <see selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" userInput="{{US_Address_CA.telephone}}" stepKey="seePhoneNumber"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml new file mode 100644 index 0000000000000..872da149ed0b2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml @@ -0,0 +1,65 @@ +<?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="AdminCreateCustomerWithCustomGroupTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, with custom group"/> + <description value="Login as admin and create customer with custom group"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5313"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="CustomCustomerGroup" stepKey="customerGroup" /> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}" /> + </actionGroup> + <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open New Customer Page --> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="$$customerGroup.code$$" stepKey="fillCustomerGroup"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <magentoCLI stepKey="flushMagentoCache" command="cache:flush" /> + <reloadPage stepKey="reloadPage"/> + + <!--Verify Customer in grid --> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail1"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForCustomerPageToLoad"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="$$customerGroup.code$$" stepKey="assertGroup"/> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> + <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + + <!--Assert Customer Form --> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <see selector="{{AdminCustomerAccountInformationSection.groupIdValue}}" userInput="$$customerGroup.code$$" stepKey="seeCustomerGroup1"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml new file mode 100644 index 0000000000000..1b901a7b3e1cd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml @@ -0,0 +1,70 @@ +<?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="AdminCreateCustomerWithPrefixTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, with prefix"/> + <description value="Login as admin and create a customer with name prefix and suffix"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5308"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open New Customer Page and create a customer with Prefix and Suffix--> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Wholesale" stepKey="fillCustomerGroup"/> + <fillField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="fillNamePrefix"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField selector="{{AdminCustomerAccountInformationSection.nameSuffix}}" userInput="{{CustomerEntityOne.suffix}}" stepKey="fillNameSuffix"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <fillField userInput="{{CustomerEntityOne.dob}}" selector="{{AdminCustomerAccountInformationSection.dateOfBirth}}" stepKey="fillDateOfBirth"/> + <selectOption userInput="Male" selector="{{AdminCustomerAccountInformationSection.gender}}" stepKey="fillGender"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <reloadPage stepKey="reloadPage"/> + + <!--Filter the customer From grid--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!-- Assert Customer in grid --> + <see userInput="{{CustomerEntityOne.prefix}} {{CustomerEntityOne.firstname}} {{CustomerEntityOne.lastname}} {{CustomerEntityOne.suffix}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="Wholesale" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertCustomerGroup"/> + <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + <see userInput="Jan 1, 1970" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertDateOfBirth"/> + <see userInput="Male" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertGender"/> + + <!--Assert Customer Form --> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="seeCustomerNamePrefix"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.nameSuffix}}" userInput="{{CustomerEntityOne.suffix}}" stepKey="seeCustomerNameSuffix"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml new file mode 100644 index 0000000000000..fe4bb3ee59e6e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml @@ -0,0 +1,61 @@ +<?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="AdminCreateCustomerWithoutAddressTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, without address"/> + <description value="Login as admin and create a customer without address"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5307"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open New Customer Page --> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <reloadPage stepKey="reloadPage"/> + + <!--Filter the customer From grid--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!-- Assert Customer in grid --> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertFirstName"/> + <see userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertLastName"/> + <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> + + <!--Assert Customer Form --> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeCustomerLastName"/> + <seeInField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeCustomerEmail"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml new file mode 100644 index 0000000000000..22ad60ff5de34 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.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="AdminCreateNewCustomerOnStorefrontSignupNewsletterTest"> + <annotations> + <stories value="Create New Customer"/> + <title value="Create New Customer on Storefront, Sign-up Newsletter"/> + <description value="Test log in to Create New Customer and Create New Customer on Storefront, Sign-up Newsletter"/> + <testCaseId value="MC-10914"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create new customer on storefront and signup news letter--> + <actionGroup ref="StorefrontCreateCustomerSignedUpNewsletterActionGroup" stepKey="createCustomer"> + <argument name="customer" value="CustomerEntityOne" /> + </actionGroup> + + <!--Assert verify created new customer in grid--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForNavigateToCustomersPageLoad"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="clickApplyFilter"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeAssertCustomerFirstNameInGrid"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="seeAssertCustomerLastNameInGrid"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAssertCustomerEmailInGrid"/> + + <!--Assert verify created new customer is subscribed to newsletter--> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickFirstRowEditLink"/> + <waitForPageLoad stepKey="waitForEditLinkLoad"/> + <click selector="{{AdminEditCustomerInformationSection.newsLetter}}" stepKey="clickNewsLetter"/> + <waitForPageLoad stepKey="waitForNewsletterTabToOpen"/> + <seeCheckboxIsChecked selector="{{AdminEditCustomerNewsletterSection.subscribedToNewsletter}}" stepKey="seeAssertSubscribedToNewsletterCheckboxIsChecked"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml new file mode 100644 index 0000000000000..fc65a271a8196 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml @@ -0,0 +1,40 @@ +<?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="AdminCreateNewCustomerOnStorefrontTest"> + <annotations> + <stories value="Create New Customer"/> + <title value="Create New Customer on Storefront"/> + <description value="Test log in to Create New Customer and Create New Customer on Storefront"/> + <testCaseId value="MC-10915"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create new customer on storefront and perform the asserts--> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + <actionGroup ref="AssertSignedUpNewsletterActionGroup" stepKey="assertSignedUpNewsLetter"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml new file mode 100644 index 0000000000000..de4ab9ffaa121 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.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="AdminCreateNewCustomerTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, new via backend"/> + <description value="Login as admin and create a new customer"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5312"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open New Customer Page --> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <reloadPage stepKey="reloadPage"/> + + <!--Filter the customer From grid--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + + <!-- Assert Customer Title --> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <see stepKey="seeCustomerTitle" selector="{{AdminEditCustomerInformationSection.customerTitle}}" userInput="{{CustomerEntityOne.firstname}} {{CustomerEntityOne.lastname}} "/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml new file mode 100644 index 0000000000000..76e4407675e4c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCustomersAllCustomersNavigateMenuTest"> + <annotations> + <features value="Customer"/> + <stories value="Menu Navigation"/> + <title value="Admin customers all customers navigate menu test"/> + <description value="Admin should be able to navigate to Customers > All Customers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14113"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAllCustomerPage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersAllCustomers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCustomersAllCustomers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml new file mode 100644 index 0000000000000..13a4b1c714337 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCustomersCustomerGroupsNavigateMenuTest"> + <annotations> + <features value="Customer"/> + <stories value="Menu Navigation"/> + <title value="Admin customers customer groups navigate menu test"/> + <description value="Admin should be able to navigate to Customers > Customer Groups"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14115"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCustomerGroupsPage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersCustomerGroups.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCustomersCustomerGroups.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml new file mode 100644 index 0000000000000..e9eb7803e01ea --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCustomersNowOnlineNavigateMenuTest"> + <annotations> + <features value="Customer"/> + <stories value="Menu Navigation"/> + <title value="Admin customers now online navigate menu test"/> + <description value="Admin should be able to navigate to Customers > Now Online"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14114"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNowOnlinePage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersNowOnline.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCustomersNowOnline.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml new file mode 100644 index 0000000000000..7fef916fc458a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.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="AdminDeleteCustomerTest"> + <annotations> + <stories value="Delete customer"/> + <title value="DeleteCustomerBackendEntityTestVariation1"/> + <description value="Login as admin and delete the customer"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14587"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create Customer --> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Delete created customer --> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="$$createCustomer.email$$"/> + </actionGroup> + <seeElement selector="{{CustomersPageSection.deletedSuccessMessage}}" stepKey="seeSuccessMessage"/> + <waitForPageLoad stepKey="waitForCustomerGridPageToLoad"/> + + <!--Assert Customer is not in Grid --> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <waitForPageLoad stepKey="waitForClearFilters1"/> + <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="$$createCustomer.email$$" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <see selector="{{AdminCustomerGridSection.customerGrid}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest.xml new file mode 100644 index 0000000000000..f58f23dee4235 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest.xml @@ -0,0 +1,308 @@ +<?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="AdminUpdateCustomerInfoFromDefaultToNonDefaultTest"> + <annotations> + <features value="Customer"/> + <stories value="Update Customer Information in Admin"/> + <title value="Update Customer Info from Default to Non-Default in Admin"/> + <description value="Update Customer Info from Default to Non-Default in Admin"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13619"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_Customer_Without_Address"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <!-- Reset customer grid filter --> + <amOnPage stepKey="goToCustomersGridPage" url="{{AdminCustomerPage.url}}"/> + <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$customer.id$$/" stepKey="openCustomerEditPage"/> + <waitForPageLoad stepKey="waitForCustomerEditPage"/> + <!-- Update Customer Account Information --> + <actionGroup stepKey="editCustomerInformation" ref="AdminEditCustomerAccountInformationActionGroup"> + <argument name="firstName" value="$$customer.firstname$$updated"/> + <argument name="lastName" value="$$customer.lastname$$updated"/> + <argument name="email" value="updated$$customer.email$$"/> + </actionGroup> + <!--Update Customer Addresses --> + <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressSetDefaultShippingAndBilling"> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup stepKey="saveAndCheckSuccessMessage" ref="AdminSaveCustomerAndAssertSuccessMessage"/> + <!-- Assert Customer in Customer grid --> + <amOnPage stepKey="goToCustomersGridPage" url="{{AdminCustomerPage.url}}"/> + <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> + <actionGroup stepKey="filterByEamil" ref="AdminFilterCustomerGridByEmail"> + <argument name="email" value="updated$$customer.email$$"/> + </actionGroup> + <actionGroup stepKey="checkCustomerInGrid" ref="AdminAssertCustomerInCustomersGrid"> + <argument name="text" value="updated$$customer.email$$"/> + <argument name="row" value="1"/> + </actionGroup> + <!-- Assert Customer in Customer Form --> + <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$customer.id$$/" stepKey="openCustomerEditPageAfterSave"/> + <waitForPageLoad stepKey="waitForCustomerEditPageAfterSave"/> + <!-- Assert Customer Account Information --> + <actionGroup stepKey="checkCustomerAccountInformation" ref="AdminAssertCustomerAccountInformation"> + <argument name="firstName" value="$$customer.firstname$$updated"/> + <argument name="lastName" value="$$customer.lastname$$updated"/> + <argument name="email" value="updated$$customer.email$$"/> + </actionGroup> + <!-- Assert Customer Default Billing Address --> + <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> + <argument name="firstName" value="$$customer.firstname$$updated"/> + <argument name="lastName" value="$$customer.lastname$$updated"/> + <argument name="street1" value="{{CustomerAddressSimple.street[0]}}"/> + <argument name="state" value="{{CustomerAddressSimple.state}}"/> + <argument name="postcode" value="{{CustomerAddressSimple.postcode}}"/> + <argument name="country" value="{{CustomerAddressSimple.country_id}}"/> + <argument name="telephone" value="{{CustomerAddressSimple.telephone}}"/> + </actionGroup> + <!-- Assert Customer Default Shipping Address --> + <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> + <argument name="firstName" value="$$customer.firstname$$updated"/> + <argument name="lastName" value="$$customer.lastname$$updated"/> + <argument name="street1" value="{{CustomerAddressSimple.street[0]}}"/> + <argument name="state" value="{{CustomerAddressSimple.state}}"/> + <argument name="postcode" value="{{CustomerAddressSimple.postcode}}"/> + <argument name="country" value="{{CustomerAddressSimple.country_id}}"/> + <argument name="telephone" value="{{CustomerAddressSimple.telephone}}"/> + </actionGroup> + </test> + + <test name="AdminUpdateCustomerAddressNoZipNoStateTest" extends="AdminUpdateCustomerInfoFromDefaultToNonDefaultTest"> + <annotations> + <features value="Customer"/> + <stories value="Update Customer Information in Admin"/> + <title value="Update Customer Address, without zip/state required, default billing/shipping checked in Admin"/> + <description value="Update Customer Address, without zip/state required, default billing/shipping checked in Admin"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13621"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <after> + <remove keyForRemoval="goToCustomersGridPage"/> + <remove keyForRemoval="waitForCustomersGrid"/> + <remove keyForRemoval="resetFilter"/> + </after> + + <!-- Remove steps that are not used for this test --> + <remove keyForRemoval="editCustomerInformation"/> + <remove keyForRemoval="goToCustomersGridPage"/> + <remove keyForRemoval="waitForCustomersGrid"/> + <remove keyForRemoval="resetFilter"/> + <remove keyForRemoval="filterByEamil"/> + <remove keyForRemoval="checkCustomerInGrid"/> + <remove keyForRemoval="checkCustomerAccountInformation"/> + + <!--Update Customer Addresses With No Zip and No State --> + <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressNoZipNoState"> + <argument name="customerAddress" value="addressNoZipNoState"/> + </actionGroup> + + <!-- Assert Customer Default Billing Address --> + <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> + <argument name="firstName" value="$$customer.firstname$$"/> + <argument name="lastName" value="$$customer.lastname$$"/> + <argument name="street1" value="{{addressNoZipNoState.street[0]}}"/> + <argument name="country" value="{{addressNoZipNoState.country_id}}"/> + <argument name="telephone" value="{{addressNoZipNoState.telephone}}"/> + </actionGroup> + <!-- Assert Customer Default Shipping Address --> + <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> + <argument name="firstName" value="$$customer.firstname$$"/> + <argument name="lastName" value="$$customer.lastname$$"/> + <argument name="street1" value="{{addressNoZipNoState.street[0]}}"/> + <argument name="country" value="{{addressNoZipNoState.country_id}}"/> + <argument name="telephone" value="{{addressNoZipNoState.telephone}}"/> + </actionGroup> + <!-- Assert Customer Login Storefront --> + <actionGroup stepKey="login" ref="StorefrontAssertSuccessLoginToStorefront" after="checkDefaultShipping" > + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + </test> + + <test name="AdminUpdateCustomerAddressNoBillingNoShippingTest" extends="AdminUpdateCustomerInfoFromDefaultToNonDefaultTest"> + <annotations> + <features value="Customer"/> + <stories value="Update Customer Information in Admin"/> + <title value="Update Customer Address, default billing/shipping unchecked in Admin"/> + <description value="Update Customer Address, default billing/shipping unchecked in Admin"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13622"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <after> + <remove keyForRemoval="goToCustomersGridPage"/> + <remove keyForRemoval="waitForCustomersGrid"/> + <remove keyForRemoval="resetFilter"/> + </after> + + <!-- Remove steps that are not used for this test --> + <remove keyForRemoval="editCustomerInformation"/> + <remove keyForRemoval="goToCustomersGridPage"/> + <remove keyForRemoval="waitForCustomersGrid"/> + <remove keyForRemoval="resetFilter"/> + <remove keyForRemoval="filterByEamil"/> + <remove keyForRemoval="checkCustomerInGrid"/> + <remove keyForRemoval="checkCustomerAccountInformation"/> + <remove keyForRemoval="checkDefaultBilling"/> + <remove keyForRemoval="checkDefaultShipping"/> + + <!--Update Customer Addresses With Default Billing and Shipping Unchecked --> + <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressesFrom"> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + + <!-- Check Customer Address in Customer Form --> + <actionGroup stepKey="checkNoDefaultBilling" ref="AdminAssertCustomerNoDefaultBillingAddress" after="waitForCustomerEditPageAfterSave"/> + <actionGroup stepKey="checkNoDefaultShipping" ref="AdminAssertCustomerNoDefaultShippingAddress" after="checkNoDefaultBilling"/> + <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerAddressGrid" after="checkNoDefaultShipping"/> + <actionGroup stepKey="searchAddress" ref="AdminFilterCustomerAddressGridByPhoneNumber" after="resetFilter"> + <argument name="phone" value="{{CustomerAddressSimple.telephone}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressStreetInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="searchAddress"> + <argument name="text" value="{{CustomerAddressSimple.street[0]}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressPhoneInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressStreetInGrid"> + <argument name="text" value="{{CustomerAddressSimple.telephone}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressStateInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressPhoneInGrid"> + <argument name="text" value="{{CustomerAddressSimple.state}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressCityInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressStateInGrid"> + <argument name="text" value="{{CustomerAddressSimple.city}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressCountryInGrid" ref="AdminAssertAddressInCustomersAddressGrid" after="checkAddressCityInGrid"> + <argument name="text" value="{{CustomerAddressSimple.country_id}}"/> + </actionGroup> + <actionGroup stepKey="resetFilterWhenDone" ref="AdminResetFilterInCustomerAddressGrid" after="checkAddressCountryInGrid"/> + <!-- Assert Customer Login Storefront --> + <actionGroup stepKey="login" ref="StorefrontAssertSuccessLoginToStorefront" after="resetFilterWhenDone" > + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + </test> + + <test name="AdminDeleteCustomerAddressTest"> + <annotations> + <features value="Customer"/> + <stories value="Delete Customer Address in Admin"/> + <title value="Delete Customer Address in Admin"/> + <description value="Delete Customer Address in Admin"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13623"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer_Multiple_Addresses"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$customer.id$$/" stepKey="openCustomerEditPage"/> + <waitForPageLoad stepKey="waitForCustomerEditPage"/> + <!-- Assert Customer Default Billing Address --> + <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> + <argument name="firstName" value="$$customer.firstname$$"/> + <argument name="lastName" value="$$customer.lastname$$"/> + <argument name="street1" value="{{US_Address_NY.street[0]}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postcode" value="{{US_Address_NY.postcode}}"/> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="telephone" value="{{US_Address_NY.telephone}}"/> + </actionGroup> + <!-- Assert Customer Default Shipping Address --> + <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> + <argument name="firstName" value="$$customer.firstname$$"/> + <argument name="lastName" value="$$customer.lastname$$"/> + <argument name="street1" value="{{US_Address_NY.street[0]}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postcode" value="{{US_Address_NY.postcode}}"/> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="telephone" value="{{US_Address_NY.telephone}}"/> + </actionGroup> + <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerAddressGrid"/> + <!-- Assert 2 records in Customer Address Grid --> + <actionGroup stepKey="see2Record" ref="AdminAssertNumberOfRecordsInCustomersAddressGrid"> + <argument name="number" value="2"/> + </actionGroup> + <!-- Assert Address 1 in Grid --> + <actionGroup stepKey="checkAddressStreetInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{US_Address_NY.street[0]}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressPhoneInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{US_Address_NY.telephone}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressStateInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{US_Address_NY.state}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressCityInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{US_Address_NY.city}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressCountryInGrid" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{US_Address_NY.country}}"/> + </actionGroup> + <!-- Assert Address 2 in Grid --> + <actionGroup stepKey="checkAddressStreetInGrid2" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{UK_Not_Default_Address.street[0]}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressPhoneInGrid2" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{UK_Not_Default_Address.telephone}}"/> + </actionGroup> + <actionGroup stepKey="checkAddressCityInGrid2" ref="AdminAssertAddressInCustomersAddressGrid"> + <argument name="text" value="{{UK_Not_Default_Address.city}}"/> + </actionGroup> + <!-- Delete Customer in Customer Address Grid --> + <actionGroup stepKey="deleteAddress" ref="AdminDeleteAddressInCustomersAddressGrid"> + <argument name="row" value="0"/> + </actionGroup> + <!-- Assert 1 record in Customer Address Grid --> + <actionGroup stepKey="see1Record" ref="AdminAssertNumberOfRecordsInCustomersAddressGrid"> + <argument name="number" value="1"/> + </actionGroup> + <actionGroup stepKey="saveAndContinue" ref="AdminCustomerSaveAndContinue"/> + <actionGroup stepKey="saveAndCheckSuccessMessage" ref="AdminSaveCustomerAndAssertSuccessMessage"/> + <!-- Assert Customer Login Storefront --> + <actionGroup stepKey="login" ref="StorefrontAssertSuccessLoginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <!-- Assert Customer Address Book --> + <actionGroup stepKey="goToAddressBook" ref="StorefrontCustomerGoToSidebarMenu"> + <argument name="menu" value="Address Book"/> + </actionGroup> + <actionGroup stepKey="assertAddressNumber" ref="StorefrontCustomerAddressBookNumberOfAddresses"> + <argument name="number" value="1"/> + </actionGroup> + <actionGroup stepKey="assertNoAddress1" ref="StorefrontCustomerAddressBookNotContains"> + <argument name="text" value="{{US_Address_NY.street[0]}}"/> + </actionGroup> + <actionGroup stepKey="assertAddress2" ref="StorefrontCustomerAddressBookContains"> + <argument name="text" value="{{UK_Not_Default_Address.street[0]}}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml new file mode 100644 index 0000000000000..7dab6eefde8ec --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml @@ -0,0 +1,39 @@ +<?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="AdminVerifyCreateCustomerRequiredFieldsTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, verify required fields on Account Information tab"/> + <description value="Login as admin and verify required fields on account information section"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5314"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open New Customer Page --> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Assert Required Fields --> + <seeElement selector="{{AdminCustomerAccountInformationSection.firstNameRequiredMessage}}" stepKey="seeFirstNameRequiredFieldMessage"/> + <seeElement selector="{{AdminCustomerAccountInformationSection.lastNameRequiredMessage}}" stepKey="seeLastNameRequiredFieldMessage"/> + <seeElement selector="{{AdminCustomerAccountInformationSection.emailRequiredMessage}}" stepKey="seeEmailRequiredFieldMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml new file mode 100644 index 0000000000000..bfb47dc9e1911 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml @@ -0,0 +1,49 @@ +<?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="AdminVerifyCustomerAddressRequiredFieldsTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, verify required fields on Addresses tab"/> + <description value="Login as admin and verify required fields on Address tab"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5315"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Created Customer --> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="editCustomerForm"> + <argument name="customer" value="Simple_Customer_Without_Address"/> + </actionGroup> + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="openAddressesTab"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForAdressPageToLoad"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToBeSaved"/> + + <!--Assert Required Field Messages --> + <seeElement selector="{{AdminCustomerAddressesSection.streetRequiredMessage}}" stepKey="seeStreetRequiredMessage"/> + <seeElement selector="{{AdminCustomerAddressesSection.cityRequiredMessage}}" stepKey="seeCityRequiredMessage"/> + <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> + <seeElement selector="{{AdminCustomerAddressesSection.countryRequiredMessage}}" stepKey="seeCountryRequiredMessage"/> + <seeElement selector="{{AdminCustomerAddressesSection.postcodeRequiredMessage}}" stepKey="seePostcodeRequiredMessage"/> + <seeElement selector="{{AdminCustomerAddressesSection.phoneNumberRequiredMessage}}" stepKey="seePhoneNumberRequiredMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml new file mode 100644 index 0000000000000..f39394ef312e4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -0,0 +1,118 @@ +<?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="AllowedCountriesRestrictionApplyOnBackendTest"> + <annotations> + <title value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> + <description value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> + <features value="Customer"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6441"/> + <useCaseId value="MAGETWO-91523"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Create new website,store and store view--> + <comment userInput="Create new website,store and store view" stepKey="createWebsite"/> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="goToAdminSystemStorePage"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateNewWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="adminCreateNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="adminCreateNewStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + </actionGroup> + <!--Set account sharing option - Default value is 'Per Website'--> + <comment userInput="Set account sharing option - Default value is 'Per Website'" stepKey="setAccountSharingOption"/> + <createData entity="CustomerAccountSharingDefault" stepKey="setToAccountSharingToDefault"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <!--delete all created data and set main website country options to default--> + <comment userInput="Delete all created data and set main website country options to default" stepKey="resetConfigToDefault"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigGeneralPage2"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> + <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + <!--Check that all countries are allowed initially and get amount--> + <comment userInput="Check that all countries are allowed initially and get amount" stepKey="checkAllCountriesAreAllowed"/> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigGeneralPage"/> + <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <executeJS function="return document.querySelectorAll('{{CountryOptionsSection.allowedCountries}} option').length" stepKey="countriesAmount"/> + <!-- Create customer for US --> + <comment userInput="Create customer for US" stepKey="createUSCustomer"/> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <!-- Switch to first website, allow only Canada and set Canada as default country --> + <comment userInput="Switch to first website, allow only Canada and set Canada as default country" stepKey="setCanadaAsDefaultCountry"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> + <waitForElementVisible selector="{{CountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible"/> + <uncheckOption selector="{{CountryOptionsSection.generalCountryDefaultInherit}}" stepKey="uncheckCheckbox1"/> + <selectOption selector="{{CountryOptionsSection.generalCountryDefault}}" userInput="Canada" stepKey="chooseCanada1"/> + <uncheckOption selector="{{CountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckCheckbox2"/> + <selectOption selector="{{CountryOptionsSection.allowedCountries}}" userInput="Canada" stepKey="chooseCanada2"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig2"/> + <waitForPageLoad stepKey="waitForSavingSystemConfiguration2"/> + <!--Switch to second website and allow all countries except Canada--> + <comment userInput="Switch to second website and allow all countries except Canada" stepKey="switchToWebsiteAndAllowOnlyCanada"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup2"> + <argument name="website" value="NewWebSiteData"/> + </actionGroup> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation3"/> + <waitForElementVisible selector="{{CountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible2"/> + <uncheckOption selector="{{CountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckCheckbox3"/> + <unselectOption selector="{{CountryOptionsSection.allowedCountries}}" userInput="Canada" stepKey="unselectCanada"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig3"/> + <waitForPageLoad stepKey="waitForSavingSystemConfiguration3"/> + <amOnPage url="{{AdminEditCustomerPage.url($$createCustomer.id$$)}}" stepKey="goToCustomerEditPage"/> + <waitForPageLoad stepKey="waitPageToLoad"/> + <!--Open created customer details page and change US address to Canada address--> + <comment userInput="Open created customer details page and change US address to Canada address" stepKey="changeCustomerAddressToCanada"/> + <actionGroup ref="OpenEditCustomerAddressFromAdminActionGroup" stepKey="editCustomerAddress"> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="Canada" stepKey="selectCountry"/> + <selectOption selector="{{AdminEditCustomerAddressesSection.state}}" userInput="Quebec" stepKey="selectState"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <click stepKey="saveCustomer" selector="{{AdminCustomerAccountInformationSection.saveCustomerAndContinueEdit}}"/> + <waitForPageLoad stepKey="waitForCustomersPage"/> + <!--Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount--> + <comment userInput="Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount" stepKey="compareCountriesAmount"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGrid"/> + <waitForPageLoad stepKey="waitForCustomersGrid"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomersGrid"/> + <executeJS function="var len = document.querySelectorAll('{{AdminCustomerFiltersSection.countryOptions}}').length; return len-1;" stepKey="countriesAmount2"/> + <assertEquals expected='($countriesAmount)' expectedType="integer" actual="($countriesAmount2)" stepKey="assertCountryAmounts"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml new file mode 100644 index 0000000000000..fb083f39ad387 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml @@ -0,0 +1,76 @@ +<?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="ChangingSingleCustomerGroupViaGrid"> + <annotations> + <title value="Change a single customer group via grid"/> + <description value="From the selection of All Customers select a single customer to change their group"/> + <severity value="MAJOR"/> + <testCaseId value="MC-10921"/> + <stories value="Change Customer Group"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created product--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="NavigateToCustomerGroupPage" stepKey="navToCustomers"/> + <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> + <argument name="customerGroupName" value="{{CustomerGroupChange.code}}"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createCustomerGroup"> + <argument name="groupName" value="{{CustomerGroupChange.code}}"/> + <argument name="taxClass" value="{{CustomerGroupChange.tax_class_name}}"/> + </actionGroup> + <actionGroup ref="NavigateToAllCustomerPage" stepKey="navToCustomers"/> + <actionGroup ref="AdminFilterCustomerByName" stepKey="filterCustomer"> + <argument name="customerName" value="{{Simple_US_Customer.fullname}}"/> + </actionGroup> + <actionGroup ref="AdminSelectCustomerByEmail" stepKey="selectCustomer"> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="SetCustomerGroupForSelectedCustomersViaGrid" stepKey="setCustomerGroup"> + <argument name="groupName" value="{{CustomerGroupChange.code}}"/> + </actionGroup> + <actionGroup ref="AdminFilterCustomerByName" stepKey="filterCustomerAfterGroupChange"> + <argument name="customerName" value="{{Simple_US_Customer.fullname}}"/> + </actionGroup> + <actionGroup ref="VerifyCustomerGroupForCustomer" stepKey="verifyCustomerGroupSet"> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + <argument name="groupName" value="{{CustomerGroupChange.code}}"/> + </actionGroup> + </test> + + <test name="ChangingAllCustomerGroupViaGrid" extends="ChangingSingleCustomerGroupViaGrid"> + <annotations> + <title value="Change all customers' group via grid"/> + <description value="Select All customers to change their group"/> + <severity value="MAJOR"/> + <testCaseId value="MC-10924"/> + <stories value="Change Customer Group"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <remove keyForRemoval="filterCustomer"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters" before="selectCustomer"/> + <actionGroup ref="AdminSelectAllCustomers" stepKey="selectCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml new file mode 100644 index 0000000000000..a085a67167f60 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.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="DeleteCustomerGroupTest"> + <annotations> + <title value="Delete customer group entity test"/> + <description value="Delete a customer group"/> + <stories value="Delete Customer Group"/> + <testCaseId value="MC-14590" /> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="CustomCustomerGroup" stepKey="customerGroup" /> + <createData entity="UsCustomerAssignedToNewCustomerGroup" stepKey="customer"> + <requiredEntity createDataKey="customerGroup" /> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Customer Group success delete message--> + <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> + <argument name="customerGroupName" value="$$customerGroup.code$$"/> + </actionGroup> + <actionGroup ref="AssertCustomerGroupNotInGridActionGroup" stepKey="assertCustomerGroupNotInGrid"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$$customer.id$$" /> + </actionGroup> + + <actionGroup ref="AssertCustomerGroupOnCustomerFormActionGroup" stepKey="assertCustomerGroupOnCustomerForm"> + <argument name="customerGroup" value="GeneralCustomerGroup" /> + </actionGroup> + + <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="openNewProductForm" /> + + <actionGroup ref="AssertCustomerGroupNotOnProductFormActionGroup" stepKey="assertCustomerGroupNotOnProductForm"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml new file mode 100644 index 0000000000000..f364d24806b9c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml @@ -0,0 +1,61 @@ +<?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="PasswordAutocompleteOffTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Password Autocomplete"/> + <title value="[Security] Autocomplete attribute with off value is added to password input"/> + <description value="[Security] Autocomplete attribute with off value is added to password input"/> + <testCaseId value="MC-13678"/> + <severity value="CRITICAL"/> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Configure Magento via CLI: disable_guest_checkout --> + <magentoCLI command="config:set checkout/options/guest_checkout 0" stepKey="disableGuestCheckout"/> + + <!-- Configure Magento via CLI: password_autocomplete_off--> + <magentoCLI command="config:set customer/password/autocomplete_on_storefront 0" stepKey="turnPasswordAutocompleteOff"/> + + <!-- Create a simple product --> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <!-- Set Magento configuration back to default values --> + <magentoCLI command="config:set checkout/options/guest_checkout 1" stepKey="disableGuestCheckoutRollback"/> + <magentoCLI command="config:set customer/password/autocomplete_on_storefront 1" stepKey="turnPasswordAutocompleteOffRollback"/> + + <!-- Delete the simple product created in the before block --> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <!-- Go to the created product page and add it to the cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <!--Click Sign in - on the top right of the page --> + <actionGroup ref="StorefrontClickSignInButtonActionGroup" stepKey="storeFrontClickSignInButton"/> + + <!--Verify if the password field on store front sign-in page has the autocomplete attribute set to off --> + <actionGroup ref="AssertStorefrontPasswordAutoCompleteOffActionGroup" stepKey="assertStorefrontPasswordAutoCompleteOff"/> + + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!--Verify if the password field on the authorization popup has the autocomplete attribute set to off --> + <actionGroup ref="AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup" stepKey="assertAuthorizationPopUpPasswordAutoCompleteOff"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml new file mode 100644 index 0000000000000..413bbfd06a539 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml @@ -0,0 +1,121 @@ +<?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="StorefrontAddNewCustomerAddressTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Storefront - My account - Address Book - add new address"/> + <description value="Storefront user should be able to create a new address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97364"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddNewCustomerAddressActionGroup" stepKey="AddNewAddress"> + <argument name="Address" value="US_Address_TX"/> + </actionGroup> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesStreetOnDefaultBilling"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesCityOnDefaultBilling"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultBilling"/> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> + </test> + <test name="StorefrontAddCustomerDefaultAddressTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Storefront - My account - Address Book - add new default billing/shipping address"/> + <description value="Storefront user should be able to create a new default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97364"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="AddNewDefaultAddress"> + <argument name="Address" value="US_Address_TX"/> + </actionGroup> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesStreetOnDefaultBilling"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesCityOnDefaultBilling"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultBilling"/> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> + </test> + <test name="StorefrontAddCustomerNonDefaultAddressTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Storefront - My account - Address Book - add new non default billing/shipping address"/> + <description value="Storefront user should be able to create a new non default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97500"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddNewCustomerAddressActionGroup" stepKey="AddNewNonDefaultAddress"> + <argument name="Address" value="US_Address_TX"/> + </actionGroup> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml index 229e81e877292..ab805193854b0 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml @@ -72,7 +72,7 @@ </actionGroup> <!--Go to My account > Address book--> - <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddressInfo"> + <actionGroup ref="EnterCustomerAddressInfoFillState" stepKey="enterAddressInfo"> <argument name="Address" value="UK_Simple_Address"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml new file mode 100644 index 0000000000000..8d4be5fda3c79 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml @@ -0,0 +1,41 @@ +<?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="StorefrontCustomerForgotPasswordTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Login"/> + <title value="Forgot Password on Storefront validates customer email input"/> + <description value="Forgot Password on Storefront validates customer email input"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13679"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="something" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickForgotPasswordLink" selector="{{StorefrontCustomerSignInFormSection.forgotPasswordLink}}"/> + <see stepKey="seePageTitle" userInput="Forgot Your Password" selector="{{StorefrontForgotPasswordSection.pageTitle}}"/> + <fillField stepKey="enterEmail" userInput="$$customer.email$$" selector="{{StorefrontForgotPasswordSection.email}}"/> + <click stepKey="clickResetPassword" selector="{{StorefrontForgotPasswordSection.resetMyPasswordButton}}"/> + <seeInCurrentUrl stepKey="seeInSignInPage" url="account/login"/> + <see stepKey="seeSuccessMessage" userInput="If there is an account associated with $$customer.email$$ you will receive an email with a link to reset your password." selector="{{StorefrontCustomerLoginMessagesSection.successMessage}}"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml new file mode 100644 index 0000000000000..104b5d56314ba --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml @@ -0,0 +1,35 @@ +<?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="StorefrontLoginWithIncorrectCredentialsTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Login"/> + <title value="Customer Login on Storefront with Incorrect Credentials"/> + <description value="Customer Login on Storefront with Incorrect Credentials"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10913"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="$$customer.password$$INVALID" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <see stepKey="seeErrorMessage" selector="{{StorefrontCustomerLoginMessagesSection.errorMessage}}" userInput="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later."/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml new file mode 100644 index 0000000000000..3121bd0da9d2d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.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="StorefrontResetCustomerPasswordFailedTest"> + <annotations> + <features value="Customer"/> + <title value="Customer tries to reset password several times"/> + <description value="Customer tries to reset password several times"/> + <severity value="CRITICAL" /> + <testCaseId value="MC-14374" /> + <group value="Customer"/> + <group value="security"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <actionGroup ref="StorefrontCustomerResetPasswordActionGroup" stepKey="resetPasswordFirstAttempt"> + <argument name="email" value="$$customer.email$$" /> + </actionGroup> + <actionGroup ref="AssertCustomerResetPasswordActionGroup" stepKey="seePageWithSuccessMessage"> + <argument name="url" value="{{StorefrontCustomerSignInPage.url}}"/> + <argument name="message" value="If there is an account associated with $$customer.email$$ you will receive an email with a link to reset your password."/> + </actionGroup> + <actionGroup ref="StorefrontCustomerResetPasswordActionGroup" stepKey="resetPasswordSecondAttempt"> + <argument name="email" value="$$customer.email$$" /> + </actionGroup> + <actionGroup ref="AssertCustomerResetPasswordActionGroup" stepKey="seePageWithErrorMessage"> + <argument name="url" value="{{StorefrontForgotPasswordPage.url}}"/> + <argument name="message" value="We received too many requests for password resets. Please wait and try again later or contact hello@example.com."/> + <argument name="messageType" value="error" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml new file mode 100644 index 0000000000000..dae456c96a679 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateCustomerAddressFranceTest"> + <annotations> + <stories value="Update Customer Address"/> + <title value="Update Customer Address (France) in Storefront"/> + <description value="Test log in to Storefront and Update Customer Address (France) in Storefront"/> + <testCaseId value="MC-10912"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address France in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerFranceAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml new file mode 100644 index 0000000000000..d9d1c9f2e05a0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.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="StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Add default customer address via the Storefront6"/> + <description value="Storefront user should be able to create a new default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97501"/> + <group value="customer"/> + <group value="update"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> + <click stepKey="ClickEditDefaultBillingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultBillingAddress}}"/> + <fillField stepKey="fillFirstName" userInput="EditedFirstNameBilling" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="EditedLastNameBilling" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + <see userInput="EditedFirstNameBilling" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultBilling"/> + <see userInput="EditedLastNameBilling" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultBilling"/> + <see userInput="{{US_Address_NY_Default_Shipping.firstname}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultShipping"/> + <see userInput="{{US_Address_NY_Default_Shipping.lastname}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultShipping"/> + </test> + <test name="StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Add default customer address via the Storefront611"/> + <description value="Storefront user should be able to create a new default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97501"/> + <group value="customer"/> + <group value="update"/> + <skip> + <issueId value="MAGETWO-97504"/> + </skip> + </annotations> + <before> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> + <click stepKey="ClickEditDefaultShippingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultShippingAddress}}"/> + <fillField stepKey="fillFirstName" userInput="EditedFirstNameShipping" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="EditedLastNameShipping" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + <see userInput="EditedFirstNameShipping" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultShipping"/> + <see userInput="EditedLastNameShipping" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultShipping"/> + <see userInput="{{US_Address_TX_Default_Billing.firstname}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultBilling"/> + <see userInput="{{US_Address_TX_Default_Billing.lastname}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultBilling"/> + </test> + <test name="StorefrontUpdateCustomerAddressFromGridTest"> + <annotations> + <features value="Customer address"/> + <stories value="Add default customer address via the Storefront7"/> + <title value="Add default customer address via the Storefront7"/> + <description value="Storefront user should be able to create a new default address via the storefront2"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97502"/> + <group value="customer"/> + <group value="update"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> + <click selector="{{StorefrontCustomerAddressesSection.editAdditionalAddress('1')}}" stepKey="editAdditionalAddress"/> + <fillField stepKey="fillFirstName" userInput="EditedFirstName" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="EditedLastName" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + <see userInput="EditedFirstName" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressFirstNameOnGrid"/> + <see userInput="EditedLastName" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressLastNameOnGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml new file mode 100644 index 0000000000000..7b6e695aa8dc4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml @@ -0,0 +1,57 @@ +<?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="StorefrontUpdateCustomerAddressUKTest"> + <annotations> + <stories value="Update Customer Address"/> + <title value="Update Customer Address (UK) in Storefront"/> + <description value="Test log in to Storefront and Update Customer Address (UK) in Storefront"/> + <testCaseId value="MC-10911"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address UK in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerUKAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddress" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerUKAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddress" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerUKAddress"/> + </actionGroup> + + <!--Verify customer name on frontend--> + <actionGroup ref="VerifyCustomerNameOnFrontend" stepKey="verifyVerifyCustomerName"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest.xml new file mode 100644 index 0000000000000..9bc253c91af92 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest.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="StorefrontUpdateCustomerPasswordValidCurrentPasswordTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Update Password"/> + <title value="Update Customer Password on Storefront, Valid Current Password"/> + <description value="Update Customer Password on Storefront, Valid Current Password"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10916"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <!--Log in to Storefront as Customer --> + <actionGroup stepKey="login" ref="LoginToStorefrontActionGroup"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <seeInCurrentUrl stepKey="onCustomerAccountPage" url="customer/account"/> + <click stepKey="clickChangePassword" selector="{{StorefrontCustomerDashboardAccountInformationSection.changePassword}}"/> + <fillField stepKey="fillValidCurrentPassword" userInput="$$customer.password$$" selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}"/> + <fillField stepKey="fillNewPassword" userInput="$$customer.password$$#" selector="{{StorefrontCustomerAccountInformationSection.newPassword}}"/> + <fillField stepKey="fillNewPasswordConfirmation" userInput="$$customer.password$$#" selector="{{StorefrontCustomerAccountInformationSection.confirmNewPassword}}"/> + <click stepKey="saveChange" selector="{{StorefrontCustomerAccountInformationSection.saveButton}}"/> + <see stepKey="verifyMessage" userInput="You saved the account information." selector="{{StorefrontCustomerMessagesSection.successMessage}}"/> + <actionGroup stepKey="logout" ref="StorefrontCustomerLogoutActionGroup"/> + <actionGroup stepKey="loginWithNewPassword" ref="LoginToStorefrontWithEmailAndPassword"> + <argument name="email" value="$$customer.email$$"/> + <argument name="password" value="$$customer.password$$#"/> + </actionGroup> + <see stepKey="seeMyEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}"/> + </test> + <test name="StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest" extends="StorefrontUpdateCustomerPasswordValidCurrentPasswordTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Update Password"/> + <title value="Update Customer Password on Storefront, Invalid Current Password"/> + <description value="Update Customer Password on Storefront, Invalid Current Password"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10917"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + + <fillField stepKey="fillValidCurrentPassword" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}"/> + <see stepKey="verifyMessage" userInput="The password doesn't match this account. Verify the password and try again." selector="{{StorefrontCustomerMessagesSection.errorMessage}}"/> + <remove keyForRemoval="loginWithNewPassword"/> + <remove keyForRemoval="seeMyEmail"/> + </test> + <test name="StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest" extends="StorefrontUpdateCustomerPasswordValidCurrentPasswordTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Update Password"/> + <title value="Update Customer Password on Storefront, Invalid Confirmation Password"/> + <description value="Update Customer Password on Storefront, Invalid Confirmation Password"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10918"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + + <fillField stepKey="fillNewPasswordConfirmation" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.confirmNewPassword}}"/> + <see stepKey="verifyMessage" userInput="Please enter the same value again." selector="{{StorefrontCustomerAccountInformationSection.confirmNewPasswordError}}"/> + <remove keyForRemoval="loginWithNewPassword"/> + <remove keyForRemoval="seeMyEmail"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml new file mode 100644 index 0000000000000..e11404db9a9a9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml @@ -0,0 +1,57 @@ +<?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="StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest"> + <annotations> + <stories value="Update Customer Address"/> + <title value="[Security] Verify No XSS Injection on Update Customer Information Add Address"/> + <description value="Test log in to Storefront and Verify No XSS Injection on Update Customer Information Add Address"/> + <testCaseId value="MC-10910"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="Colorado_US_Customer"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{Colorado_US_Customer.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerNoXSSInjection"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerNoXSSInjection"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerNoXSSInjection"/> + </actionGroup> + + <!--Verify customer name on frontend--> + <actionGroup ref="VerifyCustomerNameOnFrontend" stepKey="verifyVerifyCustomerName"> + <argument name="customer" value="Colorado_US_Customer"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml new file mode 100644 index 0000000000000..648c30b1ca0bb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml @@ -0,0 +1,38 @@ +<?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="VerifyDisabledCustomerGroupFieldTest"> + <annotations> + <stories value="Check that field is disabled in system Customer Group"/> + <title value="Check that field is disabled in system Customer Group"/> + <description value="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"/> + <testCaseId value="MC-14206"/> + <severity value="CRITICAL"/> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + + <!-- Steps --> + <!-- 1. Login to backend as admin user --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <waitForPageLoad stepKey="waitForAdminPageLoad" /> + + <!-- 2. Navigate to Customers > Customer Groups --> + <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="amOnCustomerGroupPage" /> + <waitForPageLoad stepKey="waitForCustomerGroupsPageLoad" /> + + <!-- 3. Select system Customer Group specified in data set from grid --> + <click selector="{{AdminCustomerGroupMainSection.editButtonByCustomerGroupCode(NotLoggedInCustomerGroup.code)}}" stepKey="clickOnEditCustomerGroup" /> + + <!-- 4. Perform all assertions --> + <seeInField selector="{{AdminNewCustomerGroupSection.groupName}}" userInput="{{NotLoggedInCustomerGroup.code}}" stepKey="seeNotLoggedInTextInGroupName" /> + <assertElementContainsAttribute selector="{{AdminNewCustomerGroupSection.groupName}}" attribute="disabled" expectedValue="true" stepKey="checkIfGroupNameIsDisabled" /> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php new file mode 100644 index 0000000000000..47f96b132b3db --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php @@ -0,0 +1,198 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Block\Address; + +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; + +/** + * Unit tests for \Magento\Customer\Block\Address\Grid class + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \Magento\Customer\Helper\Session\CurrentCustomer|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressCollectionFactory; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $currentCustomer; + + /** + * @var \Magento\Directory\Model\CountryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $countryFactory; + + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlBuilder; + + /** + * @var \Magento\Customer\Block\Address\Grid + */ + private $gridBlock; + + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->currentCustomer = $this->getMockBuilder(\Magento\Customer\Helper\Session\CurrentCustomer::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomer']) + ->getMock(); + + $this->addressCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->countryFactory = $this->getMockBuilder(\Magento\Directory\Model\CountryFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->urlBuilder = $this->getMockForAbstractClass(\Magento\Framework\UrlInterface::class); + + $this->gridBlock = $this->objectManager->getObject( + \Magento\Customer\Block\Address\Grid::class, + [ + 'addressCollectionFactory' => $this->addressCollectionFactory, + 'currentCustomer' => $this->currentCustomer, + 'countryFactory' => $this->countryFactory, + '_urlBuilder' => $this->urlBuilder + ] + ); + } + + /** + * Test for \Magento\Customer\Block\Address\Book::getChildHtml method with 'pager' argument + */ + public function testGetChildHtml() + { + $customerId = 1; + $outputString = 'OutputString'; + /** @var \Magento\Framework\View\Element\BlockInterface|\PHPUnit_Framework_MockObject_MockObject $block */ + $block = $this->getMockBuilder(\Magento\Framework\View\Element\BlockInterface::class) + ->setMethods(['setCollection']) + ->getMockForAbstractClass(); + /** @var $layout \Magento\Framework\View\LayoutInterface|\PHPUnit_Framework_MockObject_MockObject */ + $layout = $this->getMockForAbstractClass(\Magento\Framework\View\LayoutInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var \PHPUnit_Framework_MockObject_MockObject */ + $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setOrder', 'setCustomerFilter', 'load','addFieldToFilter']) + ->getMock(); + + $layout->expects($this->atLeastOnce())->method('getChildName')->with('NameInLayout', 'pager') + ->willReturn('ChildName'); + $layout->expects($this->atLeastOnce())->method('renderElement')->with('ChildName', true) + ->willReturn('OutputString'); + $layout->expects($this->atLeastOnce())->method('createBlock') + ->with(\Magento\Theme\Block\Html\Pager::class, 'customer.addresses.pager')->willReturn($block); + $customer->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); + $this->currentCustomer->expects($this->atLeastOnce())->method('getCustomer')->willReturn($customer); + $addressCollection->expects($this->atLeastOnce())->method('setOrder')->with('entity_id', 'desc') + ->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) + ->willReturnSelf(); + $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); + $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($addressCollection); + $block->expects($this->atLeastOnce())->method('setCollection')->with($addressCollection)->willReturnSelf(); + $this->gridBlock->setNameInLayout('NameInLayout'); + $this->gridBlock->setLayout($layout); + $this->assertEquals($outputString, $this->gridBlock->getChildHtml('pager')); + } + + /** + * Test for \Magento\Customer\Block\Address\Grid::getAddressEditUrl method + */ + public function testGetAddAddressUrl() + { + $addressId = 1; + $expectedUrl = 'expected_url'; + $this->urlBuilder->expects($this->atLeastOnce())->method('getUrl') + ->with('customer/address/edit', ['_secure' => true, 'id' => $addressId]) + ->willReturn($expectedUrl); + $this->assertEquals($expectedUrl, $this->gridBlock->getAddressEditUrl($addressId)); + } + + public function testGetAdditionalAddresses() + { + $customerId = 1; + /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var \PHPUnit_Framework_MockObject_MockObject */ + $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setOrder', 'setCustomerFilter', 'load', 'getIterator','addFieldToFilter']) + ->getMock(); + $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); + $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getDataModel']) + ->getMock(); + $collection = [$address, $address, $address]; + $address->expects($this->exactly(3))->method('getId') + ->willReturnOnConsecutiveCalls(1, 2, 3); + $address->expects($this->atLeastOnce())->method('getDataModel')->willReturn($addressDataModel); + $customer->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); + $customer->expects($this->atLeastOnce())->method('getDefaultBilling')->willReturn('1'); + $customer->expects($this->atLeastOnce())->method('getDefaultShipping')->willReturn('2'); + + $this->currentCustomer->expects($this->atLeastOnce())->method('getCustomer')->willReturn($customer); + $addressCollection->expects($this->atLeastOnce())->method('setOrder')->with('entity_id', 'desc') + ->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) + ->willReturnSelf(); + $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('getIterator') + ->willReturn(new \ArrayIterator($collection)); + $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($addressCollection); + + $this->assertEquals($addressDataModel, $this->gridBlock->getAdditionalAddresses()[0]); + } + + /** + * Test for \Magento\Customer\ViewModel\CustomerAddress::getStreetAddress method + */ + public function testGetStreetAddress() + { + $street = ['Line 1', 'Line 2']; + $expectedAddress = 'Line 1, Line 2'; + $address = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); + $address->expects($this->atLeastOnce())->method('getStreet')->willReturn($street); + $this->assertEquals($expectedAddress, $this->gridBlock->getStreetAddress($address)); + } + + /** + * Test for \Magento\Customer\ViewModel\CustomerAddress::getCountryByCode method + */ + public function testGetCountryByCode() + { + $countryId = 'US'; + $countryName = 'United States'; + $country = $this->getMockBuilder(\Magento\Directory\Model\Country::class) + ->disableOriginalConstructor() + ->setMethods(['loadByCode', 'getName']) + ->getMock(); + $this->countryFactory->expects($this->atLeastOnce())->method('create')->willReturn($country); + $country->expects($this->atLeastOnce())->method('loadByCode')->with($countryId)->willReturnSelf(); + $country->expects($this->atLeastOnce())->method('getName')->willReturn($countryName); + $this->assertEquals($countryName, $this->gridBlock->getCountryByCode($countryId)); + } +} 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 c2a795fc95016..7ae55f44421c7 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php @@ -455,14 +455,20 @@ public function testExecute( $regionCode, $newRegionId, $newRegion, - $newRegionCode + $newRegionCode, + $existingDefaultBilling = false, + $existingDefaultShipping = false, + $setDefaultBilling = false, + $setDefaultShipping = false ): void { $existingAddressData = [ 'country_id' => $countryId, 'region_id' => $regionId, 'region' => $region, 'region_code' => $regionCode, - 'customer_id' => $customerId + 'customer_id' => $customerId, + 'default_billing' => $existingDefaultBilling, + 'default_shipping' => $existingDefaultShipping, ]; $newAddressData = [ 'country_id' => $countryId, @@ -486,8 +492,8 @@ public function testExecute( ->method('getParam') ->willReturnMap([ ['id', null, $addressId], - ['default_billing', false, $addressId], - ['default_shipping', false, $addressId], + ['default_billing', $existingDefaultBilling, $setDefaultBilling], + ['default_shipping', $existingDefaultShipping, $setDefaultShipping], ]); $this->addressRepository->expects($this->once()) @@ -565,11 +571,11 @@ public function testExecute( ->willReturnSelf(); $this->addressData->expects($this->once()) ->method('setIsDefaultBilling') - ->with() + ->with($setDefaultBilling) ->willReturnSelf(); $this->addressData->expects($this->once()) ->method('setIsDefaultShipping') - ->with() + ->with($setDefaultShipping) ->willReturnSelf(); $this->messageManager->expects($this->once()) @@ -628,11 +634,11 @@ public function dataProviderTestExecute(): array [1, 1, 1, 2, null, null, 12, null, null], [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA'], - [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null], + [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null, true, true, true, false], - [1, 1, 1, 2, null, null, 12, null, null], - [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA'], - [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null], + [1, 1, 1, 2, null, null, 12, null, null, false, false, true, false], + [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA', true, false, true, false], + [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null, true, true, true, true], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php index 78d9dd7003522..45e64f6557d51 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php @@ -5,10 +5,14 @@ */ namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\EmailNotificationInterface; +use Magento\Framework\DataObject; use Magento\Framework\Message\MessageInterface; /** + * Unit tests for Inline customer edit + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -68,14 +72,27 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase /** @var EmailNotificationInterface|\PHPUnit_Framework_MockObject_MockObject */ private $emailNotification; + /** @var AddressRegistry|\PHPUnit_Framework_MockObject_MockObject */ + private $addressRegistry; + /** @var array */ private $items; + /** + * Sets up mocks + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class, [], '', false); + $this->request = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false + ); $this->messageManager = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class, [], @@ -125,8 +142,12 @@ protected function setUp() '', false ); - $this->logger = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class, [], '', false); - + $this->logger = $this->getMockForAbstractClass( + \Psr\Log\LoggerInterface::class, + [], + '', + false + ); $this->emailNotification = $this->getMockBuilder(EmailNotificationInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -138,6 +159,7 @@ protected function setUp() 'messageManager' => $this->messageManager, ] ); + $this->addressRegistry = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); $this->controller = $objectManager->getObject( \Magento\Customer\Controller\Adminhtml\Index\InlineEdit::class, [ @@ -150,6 +172,7 @@ protected function setUp() 'addressDataFactory' => $this->addressDataFactory, 'addressRepository' => $this->addressRepository, 'logger' => $this->logger, + 'addressRegistry' => $this->addressRegistry ] ); $reflection = new \ReflectionClass(get_class($this->controller)); @@ -166,6 +189,8 @@ protected function setUp() } /** + * Prepare mocks for tests + * * @param int $populateSequence */ protected function prepareMocksForTesting($populateSequence = 0) @@ -204,6 +229,9 @@ protected function prepareMocksForTesting($populateSequence = 0) ->willReturn(12); } + /** + * Prepare mocks for update customers default billing address use case + */ protected function prepareMocksForUpdateDefaultBilling() { $this->prepareMocksForProcessAddressData(); @@ -212,12 +240,15 @@ protected function prepareMocksForUpdateDefaultBilling() 'firstname' => 'Firstname', 'lastname' => 'Lastname', ]; - $this->customerData->expects($this->once()) + $this->customerData->expects($this->exactly(2)) ->method('getAddresses') ->willReturn([$this->address]); $this->address->expects($this->once()) ->method('isDefaultBilling') ->willReturn(true); + $this->addressRegistry->expects($this->once()) + ->method('retrieve') + ->willReturn(new DataObject()); $this->dataObjectHelper->expects($this->at(0)) ->method('populateWithArray') ->with( @@ -227,6 +258,9 @@ protected function prepareMocksForUpdateDefaultBilling() ); } + /** + * Prepare mocks for processing customers address data use case + */ protected function prepareMocksForProcessAddressData() { $this->customerData->expects($this->once()) @@ -237,6 +271,9 @@ protected function prepareMocksForProcessAddressData() ->willReturn('Lastname'); } + /** + * Prepare mocks for error messages processing test + */ protected function prepareMocksForErrorMessagesProcessing() { $this->messageManager->expects($this->atLeastOnce()) @@ -261,6 +298,9 @@ protected function prepareMocksForErrorMessagesProcessing() ->willReturnSelf(); } + /** + * Unit test for updating customers billing address use case + */ public function testExecuteWithUpdateBilling() { $this->prepareMocksForTesting(1); @@ -281,6 +321,9 @@ public function testExecuteWithUpdateBilling() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for creating customer with empty data use case + */ public function testExecuteWithoutItems() { $this->resultJsonFactory->expects($this->once()) @@ -305,6 +348,9 @@ public function testExecuteWithoutItems() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for verifying Localized Exception during inline edit + */ public function testExecuteLocalizedException() { $exception = new \Magento\Framework\Exception\LocalizedException(__('Exception message')); @@ -312,6 +358,9 @@ public function testExecuteLocalizedException() $this->customerData->expects($this->once()) ->method('getDefaultBilling') ->willReturn(false); + $this->customerData->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository->expects($this->once()) ->method('save') ->with($this->customerData) @@ -327,6 +376,9 @@ public function testExecuteLocalizedException() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for verifying Execute Exception during inline edit + */ public function testExecuteException() { $exception = new \Exception('Exception message'); @@ -334,6 +386,9 @@ public function testExecuteException() $this->customerData->expects($this->once()) ->method('getDefaultBilling') ->willReturn(false); + $this->customerData->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository->expects($this->once()) ->method('save') ->with($this->customerData) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php index 884aab711d168..10144bdc318c1 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -11,6 +11,7 @@ /** * Class MassAssignGroupTest + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase @@ -70,12 +71,17 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase */ protected $customerRepositoryMock; + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + $resultRedirectFactory = $this->createMock( + \Magento\Backend\Model\View\Result\RedirectFactory::class + ); $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor()->getMock(); @@ -129,7 +135,8 @@ protected function setUp() $this->customerCollectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->customerCollectionMock); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) + $this->customerRepositoryMock = $this + ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMockForAbstractClass(); $this->massAction = $objectManagerHelper->getObject( \Magento\Customer\Controller\Adminhtml\Index\MassAssignGroup::class, @@ -142,12 +149,18 @@ protected function setUp() ); } + /** + * Unit test to verify mass customer group assignment use case + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecute() { $customersIds = [10, 11, 12]; - $customerMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class - )->getMockForAbstractClass(); + $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['setData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->customerCollectionMock->expects($this->any()) ->method('getAllIds') ->willReturn($customersIds); @@ -168,6 +181,11 @@ public function testExecute() $this->massAction->execute(); } + /** + * Unit test to verify expected error during mass customer group assignment use case + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecuteWithException() { $customersIds = [10, 11, 12]; 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 c52d5b2fb370f..57f384d32d980 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 @@ -14,6 +14,8 @@ use Magento\Framework\Controller\Result\Redirect; /** + * Testing Save Customer use case from admin page + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @covers \Magento\Customer\Controller\Adminhtml\Index\Save @@ -435,6 +437,10 @@ public function testExecuteWithExistentCustomer() $customerEmail = 'customer@email.com'; $customerMock->expects($this->once())->method('getEmail')->willReturn($customerEmail); + $customerMock->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); + $this->emailNotificationMock->expects($this->once()) ->method('credentialsChanged') ->with($customerMock, $customerEmail) @@ -693,22 +699,24 @@ public function testExecuteWithNewCustomerAndValidationException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -731,12 +739,12 @@ public function testExecuteWithNewCustomerAndValidationException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -744,19 +752,19 @@ public function testExecuteWithNewCustomerAndValidationException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -804,7 +812,10 @@ public function testExecuteWithNewCustomerAndValidationException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -835,22 +846,24 @@ public function testExecuteWithNewCustomerAndLocalizedException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -873,12 +886,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -887,19 +900,19 @@ public function testExecuteWithNewCustomerAndLocalizedException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -946,7 +959,10 @@ public function testExecuteWithNewCustomerAndLocalizedException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -977,22 +993,24 @@ public function testExecuteWithNewCustomerAndException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -1015,12 +1033,12 @@ public function testExecuteWithNewCustomerAndException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -1028,19 +1046,19 @@ public function testExecuteWithNewCustomerAndException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -1089,7 +1107,10 @@ public function testExecuteWithNewCustomerAndException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) diff --git a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php index 2c49a81676c1e..0818d94afe57c 100644 --- a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php @@ -415,32 +415,6 @@ public function isAttributeVisibleDataProvider() ]; } - /** - * Test is required filed by attribute code - * - * @param string $attributeCode - * @param bool $isMetadataExists - * @dataProvider isAttributeRequiredDataProvider - * @covers \Magento\Customer\Helper\Address::isAttributeRequired() - * @return void - */ - public function testIsAttributeRequired($attributeCode, $isMetadataExists) - { - $attributeMetadata = null; - if ($isMetadataExists) { - $attributeMetadata = $this->getMockBuilder(\Magento\Customer\Api\Data\AttributeMetadataInterface::class) - ->getMockForAbstractClass(); - $attributeMetadata->expects($this->once()) - ->method('isRequired') - ->willReturn(true); - } - $this->addressMetadataService->expects($this->once()) - ->method('getAttributeMetadata') - ->with($attributeCode) - ->willReturn($attributeMetadata); - $this->assertEquals($isMetadataExists, $this->helper->isAttributeRequired($attributeCode)); - } - /** * Data provider for test testIsAttributeRequire * diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index 0273c445bdd2a..209a9b077a307 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -1238,8 +1238,7 @@ public function testInitiatePasswordResetEmailReminder() $storeId = 1; - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); + $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); $this->emailNotificationMock->expects($this->once()) ->method('passwordReminder') @@ -1263,8 +1262,7 @@ public function testInitiatePasswordResetEmailReset() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); + $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); $this->emailNotificationMock->expects($this->once()) ->method('passwordResetConfirmation') @@ -1288,8 +1286,7 @@ public function testInitiatePasswordResetNoTemplate() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); + $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); @@ -1472,12 +1469,15 @@ public function testChangePassword() $passwordHash = '1a2b3f4c'; $this->reInitModel(); - $customer = $this->getMockBuilder(Customer::class) + $customer = $this->getMockBuilder(CustomerInterface::class) ->disableOriginalConstructor() ->getMock(); $customer->expects($this->any()) ->method('getId') ->willReturn($customerId); + $customer->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository ->expects($this->once()) @@ -1607,7 +1607,7 @@ function ($string) { $this->customerSecure->expects($this->once())->method('setRpTokenCreatedAt')->with(null); $this->customerSecure->expects($this->any())->method('setPasswordHash')->willReturn(null); - $this->sessionManager->expects($this->atLeastOnce())->method('destroy'); + $this->sessionManager->method('isSessionExists')->willReturn(false); $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); $visitor = $this->getMockBuilder(\Magento\Customer\Model\Visitor::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php index cb396e32509b7..65831069aa1fb 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php @@ -13,9 +13,12 @@ use Magento\Customer\Model\Customer; use Magento\Customer\Model\AccountConfirmation; +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class CustomerTest extends \PHPUnit\Framework\TestCase { @@ -68,6 +71,21 @@ class CustomerTest extends \PHPUnit\Framework\TestCase */ private $accountConfirmation; + /** + * @var AddressCollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressesFactory; + + /** + * @var CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerDataFactory; + + /** + * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataObjectHelper; + protected function setUp() { $this->_website = $this->createMock(\Magento\Store\Model\Website::class); @@ -100,6 +118,19 @@ protected function setUp() $this->_encryptor = $this->createMock(\Magento\Framework\Encryption\EncryptorInterface::class); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->addressesFactory = $this->getMockBuilder(AddressCollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->customerDataFactory = $this->getMockBuilder(CustomerInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) + ->disableOriginalConstructor() + ->setMethods(['populateWithArray']) + ->getMock(); + $this->_model = $helper->getObject( \Magento\Customer\Model\Customer::class, [ @@ -112,7 +143,10 @@ protected function setUp() 'registry' => $this->registryMock, 'resource' => $this->resourceMock, 'dataObjectProcessor' => $this->dataObjectProcessor, - 'accountConfirmation' => $this->accountConfirmation + 'accountConfirmation' => $this->accountConfirmation, + '_addressesFactory' => $this->addressesFactory, + 'customerDataFactory' => $this->customerDataFactory, + 'dataObjectHelper' => $this->dataObjectHelper ] ); } @@ -186,13 +220,13 @@ public function testSendNewAccountEmailWithoutStoreId() ->will($this->returnValue($transportMock)); $this->_model->setData([ - 'website_id' => 1, - 'store_id' => 1, - 'email' => 'email@example.com', - 'firstname' => 'FirstName', - 'lastname' => 'LastName', - 'middlename' => 'MiddleName', - 'prefix' => 'Name Prefix', + 'website_id' => 1, + 'store_id' => 1, + 'email' => 'email@example.com', + 'firstname' => 'FirstName', + 'lastname' => 'LastName', + 'middlename' => 'MiddleName', + 'prefix' => 'Name Prefix', ]); $this->_model->sendNewAccountEmail('registered'); } @@ -310,4 +344,43 @@ public function testUpdateData() $this->assertEquals($this->_model->getData(), $expectedResult); } + + /** + * Test for the \Magento\Customer\Model\Customer::getDataModel() method + */ + public function testGetDataModel() + { + $customerId = 1; + $this->_model->setEntityId($customerId); + $this->_model->setId($customerId); + $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); + $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + ->disableOriginalConstructor() + ->setMethods(['setCustomer', 'getDataModel']) + ->getMock(); + $address->expects($this->atLeastOnce())->method('getDataModel')->willReturn($addressDataModel); + $addresses = new \ArrayIterator([$address, $address]); + $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setCustomerFilter', 'addAttributeToSelect', 'getIterator', 'getItems']) + ->getMock(); + $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('addAttributeToSelect')->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('getIterator') + ->willReturn($addresses); + $addressCollection->expects($this->atLeastOnce())->method('getItems') + ->willReturn($addresses); + $this->addressesFactory->expects($this->atLeastOnce())->method('create')->willReturn($addressCollection); + $customerDataObject = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->customerDataFactory->expects($this->atLeastOnce())->method('create')->willReturn($customerDataObject); + $this->dataObjectHelper->expects($this->atLeastOnce())->method('populateWithArray') + ->with($customerDataObject, $this->_model->getData(), \Magento\Customer\Api\Data\CustomerInterface::class) + ->willReturnSelf(); + $customerDataObject->expects($this->atLeastOnce())->method('setAddresses') + ->with([$addressDataModel, $addressDataModel]) + ->willReturnSelf(); + $customerDataObject->expects($this->atLeastOnce())->method('setId')->with($customerId)->willReturnSelf(); + $this->_model->getDataModel(); + $this->assertEquals($customerDataObject, $this->_model->getDataModel()); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php index 658472d13ab93..83915731ea5a9 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php @@ -15,7 +15,14 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +/** + * AttributeMetadataCache Test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase { /** @@ -43,6 +50,16 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase */ private $attributeMetadataCache; + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -50,13 +67,18 @@ protected function setUp() $this->stateMock = $this->createMock(StateInterface::class); $this->serializerMock = $this->createMock(SerializerInterface::class); $this->attributeMetadataHydratorMock = $this->createMock(AttributeMetadataHydrator::class); + $this->storeMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); + $this->storeMock->method('getId')->willReturn(1); $this->attributeMetadataCache = $objectManager->getObject( AttributeMetadataCache::class, [ 'cache' => $this->cacheMock, 'state' => $this->stateMock, 'serializer' => $this->serializerMock, - 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock + 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock, + 'storeManager' => $this->storeManagerMock ] ); } @@ -80,7 +102,8 @@ public function testLoadNoCache() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $this->stateMock->expects($this->once()) ->method('isEnabled') ->with(Type::TYPE_IDENTIFIER) @@ -96,7 +119,8 @@ public function testLoad() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedString = 'serialized string'; $attributeMetadataOneData = [ 'attribute_code' => 'attribute_code', @@ -156,7 +180,8 @@ public function testSave() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedString = 'serialized string'; $attributeMetadataOneData = [ 'attribute_code' => 'attribute_code', diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php index e4dc22ba40e31..5b4b50ca82117 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php @@ -205,37 +205,32 @@ public function applyOutputFilterDataProvider() } /** + * Tests input validation rules. + * * @param null|string $value * @param null|string $label * @param null|string $inputValidation * @param bool|array $expectedOutput * @dataProvider validateInputRuleDataProvider */ - public function testValidateInputRule($value, $label, $inputValidation, $expectedOutput) + public function testValidateInputRule($value, $label, $inputValidation, $expectedOutput): void { $validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); - $validationRule->expects($this->any()) - ->method('getName') - ->will($this->returnValue('input_validation')); - $validationRule->expects($this->any()) - ->method('getValue') - ->will($this->returnValue($inputValidation)); - - $this->_attributeMock->expects($this->any())->method('getStoreLabel')->will($this->returnValue($label)); - $this->_attributeMock->expects( - $this->any() - )->method( - 'getValidationRules' - )->will( - $this->returnValue( - [ - $validationRule, - ] - ) - ); + + $validationRule->method('getName') + ->willReturn('input_validation'); + + $validationRule->method('getValue') + ->willReturn($inputValidation); + + $this->_attributeMock->method('getStoreLabel') + ->willReturn($label); + + $this->_attributeMock->method('getValidationRules') + ->willReturn([$validationRule]); $this->assertEquals($expectedOutput, $this->_model->validateInputRule($value)); } @@ -256,6 +251,16 @@ public function validateInputRuleDataProvider() \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' ] ], + [ + 'abc qaz', + 'mylabel', + 'alphanumeric', + [ + \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' + ] + ], + ['abcqaz', 'mylabel', 'alphanumeric', true], + ['abc qaz', 'mylabel', 'alphanum-with-spaces', true], [ '!@#$', 'mylabel', diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php index c655ff7056ed6..e67adc47b8884 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Test\Unit\Model\Renderer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + class RegionTest extends \PHPUnit\Framework\TestCase { /** @@ -58,6 +60,14 @@ public function testRender($regionCollection) ] ) ); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($elementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($elementMock, $escaper); + $formMock->expects( $this->any() )->method( diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php index 8971f155f782e..188bbde71c104 100644 --- a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Observer\UpgradeCustomerPasswordObserver; +/** + * Class UpgradeCustomerPasswordObserverTest for testing upgrade password observer + */ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -29,9 +32,13 @@ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase */ protected $customerRegistry; + /** + * @inheritdoc + */ protected function setUp() { - $this->customerRepository = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) + $this->customerRepository = $this + ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMockForAbstractClass(); $this->customerRegistry = $this->getMockBuilder(\Magento\Customer\Model\CustomerRegistry::class) ->disableOriginalConstructor() @@ -47,6 +54,9 @@ protected function setUp() ); } + /** + * Unit test for verifying customers password upgrade observer + */ public function testUpgradeCustomerPassword() { $customerId = '1'; @@ -57,6 +67,8 @@ public function testUpgradeCustomerPassword() ->setMethods(['getId']) ->getMock(); $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['setData']) + ->disableOriginalConstructor() ->getMockForAbstractClass(); $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php index 130b3acd11e76..07b0a76043200 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php @@ -18,12 +18,6 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->validationRules = $this->getMockBuilder( - \Magento\Customer\Ui\Component\Listing\Column\ValidationRules::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -31,20 +25,26 @@ protected function setUp() $this->validationRules = new ValidationRules(); } - public function testGetValidationRules() + /** + * Tests input validation rules + * + * @param String $validationRule - provided input validation rules + * @param String $validationClass - expected input validation class + * @dataProvider validationRulesDataProvider + */ + public function testGetValidationRules(String $validationRule, String $validationClass): void { $expectsRules = [ 'required-entry' => true, - 'validate-number' => true, + $validationClass => true, ]; - $this->validationRule->expects($this->atLeastOnce()) - ->method('getName') + $this->validationRule->method('getName') ->willReturn('input_validation'); - $this->validationRule->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn('numeric'); - $this->assertEquals( + $this->validationRule->method('getValue') + ->willReturn($validationRule); + + self::assertEquals( $expectsRules, $this->validationRules->getValidationRules( true, @@ -56,6 +56,23 @@ public function testGetValidationRules() ); } + /** + * Provides possible validation rules. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'validate-email'] + ]; + } + public function testGetValidationRulesWithOnlyRequiredRule() { $expectsRules = [ diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php index b8f83421a6d62..6befec8e942a1 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php @@ -7,6 +7,9 @@ use Magento\Customer\Api\Data\ValidationRuleInterface; +/** + * Provides validation classes according to corresponding rules. + */ class ValidationRules { /** @@ -16,6 +19,7 @@ class ValidationRules 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'validate-email', ]; diff --git a/app/code/Magento/Customer/etc/acl.xml b/app/code/Magento/Customer/etc/acl.xml index e8e6219bef4fe..1d45aa6445db8 100644 --- a/app/code/Magento/Customer/etc/acl.xml +++ b/app/code/Magento/Customer/etc/acl.xml @@ -10,7 +10,13 @@ <resources> <resource id="Magento_Backend::admin"> <resource id="Magento_Customer::customer" title="Customers" translate="title" sortOrder="40"> - <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10" /> + <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10"> + <resource id="Magento_Customer::actions" title="Actions" translate="title" sortOrder="10"> + <resource id="Magento_Customer::delete" title="Delete" translate="title" sortOrder="10" /> + <resource id="Magento_Customer::reset_password" title="Reset password" translate="title" sortOrder="20" /> + <resource id="Magento_Customer::invalidate_tokens" title="Invalidate tokens" translate="title" sortOrder="30" /> + </resource> + </resource> <resource id="Magento_Customer::online" title="Now Online" translate="title" sortOrder="20" /> <resource id="Magento_Customer::group" title="Customer Groups" translate="title" sortOrder="30" /> </resource> diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 4a45c4ad48d19..c31742519e581 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -57,7 +57,7 @@ <type name="Magento\Checkout\Block\Cart\Sidebar"> <plugin name="customer_cart" type="Magento\Customer\Model\Cart\ConfigPlugin" /> </type> - <type name="Magento\Framework\Session\SessionManager"> + <type name="Magento\Framework\Session\SessionManagerInterface"> <plugin name="session_checker" type="Magento\Customer\CustomerData\Plugin\SessionChecker" /> </type> <type name="Magento\Authorization\Model\CompositeUserContext"> @@ -77,4 +77,4 @@ </argument> </arguments> </type> -</config> +</config> \ No newline at end of file diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml index 6b479ad1cb290..f845d407d401a 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml @@ -174,6 +174,7 @@ </column> <column name="billing_country_id" component="Magento_Ui/js/grid/columns/select" sortOrder="80"> <settings> + <options class="Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites"/> <filter>select</filter> <dataType>select</dataType> <label translate="true">Country</label> diff --git a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html index 9b183d63471f3..010087ace2d42 100644 --- a/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/account_new_confirmation.html @@ -9,9 +9,7 @@ "var this.getUrl($store, 'customer/account/confirm/', [_query:[id:$customer.id, key:$customer.confirmation, back_url:$back_url]])":"Account Confirmation URL", "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var customer.email":"Customer Email", -"var customer.name":"Customer Name", -"var extensions":"Extensions", -"var url":"Url" +"var customer.name":"Customer Name" } @--> {{template config_path="design/email/header_template"}} @@ -25,7 +23,7 @@ <table class="inner-wrapper" border="0" cellspacing="0" cellpadding="0" align="center"> <tr> <td align="center"> - <a href="{{var this.getUrl($store,$url,[_query:[id:$customer.id,key:$customer.confirmation,extensions:$extensions,back_url:$back_url],_nosid:1])}}" target="_blank">{{trans "Confirm Your Account"}}</a> + <a href="{{var this.getUrl($store,'customer/account/confirm/',[_query:[id:$customer.id,key:$customer.confirmation,back_url:$back_url],_nosid:1])}}" target="_blank">{{trans "Confirm Your Account"}}</a> </td> </tr> </table> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml index fd5ecbfa7f277..0c5af453f2373 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <title>Create New Customer Account + diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml index d49dae6dee58f..3518df736c4ac 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -6,6 +6,9 @@ */ --> + + Customer Login + diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml index f053805409fe5..f5ee2b347a5b2 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml @@ -20,6 +20,7 @@ Magento\Customer\Block\DataProviders\AddressAttributeData + Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml index bad120e46277f..2c5e5b98e5f7b 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml @@ -13,6 +13,7 @@ +
diff --git a/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml b/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml index 2bdb6ac044a92..b7b8796142de8 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml @@ -16,7 +16,6 @@ data-toggle="dropdown" data-trigger-keypress-button="true" data-bind="scope: 'customer'"> -
- - - - diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml index faa15f7240235..df3f000410830 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -126,6 +126,9 @@ title="getAttributeData()->getFrontendLabel('postcode') ?>" id="zip" class="input-text validate-zip-international escapeHtmlAttr($this->helper('Magento\Customer\Helper\Address')->getAttributeValidationClass('postcode')) ?>"> +
@@ -184,7 +187,9 @@ diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index 0a37896b810c4..31510a65ef741 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -29,7 +29,7 @@ $fieldCssClass = 'field date field-' . $block->getHtmlId(); $fieldCssClass .= $block->isRequired() ? ' required' : ''; ?>
- +
getFieldHtml() ?> getAdditionalDescription()) : ?> diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml index d60f0968ad4fe..8b45618a891ef 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml @@ -9,9 +9,9 @@ /** @var \Magento\Customer\Block\Widget\Gender $block */ ?>
- +
- isRequired()):?> class="validate-select" data-validate="{required:true}"> getGenderOptions(); ?> getGender();?> diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml index 4b3681d4d8fd3..bb60845a64e6d 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml @@ -9,8 +9,8 @@ /** @var \Magento\Customer\Block\Widget\Taxvat $block */ ?>
- +
- isRequired()) echo ' data-validate="{required:true}"' ?>> + isRequired()) echo ' data-validate="{required:true}"' ?>>
diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml index 1b61dc45573b3..6367bf10bbade 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml @@ -19,7 +19,7 @@ escapeHtmlAttr( $this->helper('Magento\Customer\Helper\Address') - ->getAttributeValidationClass('fax') + ->getAttributeValidationClass('telephone') ); ?> getAllowedAddressAttributes = $getAllowedAddressAttributes; + $this->addressFactory = $addressFactory; + $this->addressRepository = $addressRepository; + $this->dataObjectHelper = $dataObjectHelper; + } + + /** + * Create customer address + * + * @param int $customerId + * @param array $data + * @return AddressInterface + * @throws GraphQlInputException + */ + public function execute(int $customerId, array $data): AddressInterface + { + $this->validateData($data); + + /** @var AddressInterface $address */ + $address = $this->addressFactory->create(); + $this->dataObjectHelper->populateWithArray($address, $data, AddressInterface::class); + + if (isset($data['region']['region_id'])) { + $address->setRegionId($address->getRegion()->getRegionId()); + } + $address->setCustomerId($customerId); + + try { + $this->addressRepository->save($address); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + return $address; + } + + /** + * Validate customer address create data + * + * @param array $addressData + * @return void + * @throws GraphQlInputException + */ + public function validateData(array $addressData): void + { + $attributes = $this->getAllowedAddressAttributes->execute(); + $errorInput = []; + + foreach ($attributes as $attributeName => $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($addressData[$attributeName]) || empty($addressData[$attributeName])) + ) { + $errorInput[] = $attributeName; + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressCreateDataValidator.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressCreateDataValidator.php deleted file mode 100644 index 65672bcd3503b..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressCreateDataValidator.php +++ /dev/null @@ -1,56 +0,0 @@ -getAllowedAddressAttributes = $getAllowedAddressAttributes; - } - - /** - * Validate customer address create data - * - * @param array $addressData - * @return void - * @throws GraphQlInputException - */ - public function validate(array $addressData): void - { - $attributes = $this->getAllowedAddressAttributes->execute(); - $errorInput = []; - - foreach ($attributes as $attributeName => $attributeInfo) { - if ($attributeInfo->getIsRequired() - && (!isset($addressData[$attributeName]) || empty($addressData[$attributeName])) - ) { - $errorInput[] = $attributeName; - } - } - - if ($errorInput) { - throw new GraphQlInputException( - __('Required parameters are missing: %1', [implode(', ', $errorInput)]) - ); - } - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressDataProvider.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressDataProvider.php deleted file mode 100644 index 9640953032ac6..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressDataProvider.php +++ /dev/null @@ -1,127 +0,0 @@ -serviceOutputProcessor = $serviceOutputProcessor; - $this->jsonSerializer = $jsonSerializer; - $this->customerResourceModel = $customerResourceModel; - $this->customerFactory = $customerFactory; - } - - /** - * Curate shipping and billing default options - * - * @param array $address - * @param AddressInterface $addressObject - * @return array - */ - private function curateAddressDefaultValues(array $address, AddressInterface $addressObject) : array - { - $customerModel = $this->customerFactory->create(); - $this->customerResourceModel->load($customerModel, $addressObject->getCustomerId()); - $address[CustomerInterface::DEFAULT_BILLING] = - ($customerModel->getDefaultBillingAddress() - && $addressObject->getId() == $customerModel->getDefaultBillingAddress()->getId()); - $address[CustomerInterface::DEFAULT_SHIPPING] = - ($customerModel->getDefaultShippingAddress() - && $addressObject->getId() == $customerModel->getDefaultShippingAddress()->getId()); - return $address; - } - - /** - * Transform single customer address data from object to in array format - * - * @param AddressInterface $addressObject - * @return array - */ - public function getAddressData(AddressInterface $addressObject): array - { - $address = $this->serviceOutputProcessor->process( - $addressObject, - AddressRepositoryInterface::class, - 'getById' - ); - $address = $this->curateAddressDefaultValues($address, $addressObject); - - if (isset($address[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY])) { - $address = array_merge($address, $address[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY]); - } - $customAttributes = []; - if (isset($address[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { - foreach ($address[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } - } - $address = array_merge($address, $customAttributes); - - return $address; - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressUpdateDataValidator.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressUpdateDataValidator.php deleted file mode 100644 index 13716b491fddf..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CustomerAddressUpdateDataValidator.php +++ /dev/null @@ -1,56 +0,0 @@ -getAllowedAddressAttributes = $getAllowedAddressAttributes; - } - - /** - * Validate customer address update data - * - * @param array $addressData - * @return void - * @throws GraphQlInputException - */ - public function validate(array $addressData): void - { - $attributes = $this->getAllowedAddressAttributes->execute(); - $errorInput = []; - - foreach ($attributes as $attributeName => $attributeInfo) { - if ($attributeInfo->getIsRequired() - && (isset($addressData[$attributeName]) && empty($addressData[$attributeName])) - ) { - $errorInput[] = $attributeName; - } - } - - if ($errorInput) { - throw new GraphQlInputException( - __('Required parameters are missing: %1', [implode(', ', $errorInput)]) - ); - } - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/DeleteCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/DeleteCustomerAddress.php new file mode 100644 index 0000000000000..586fbebde703f --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/DeleteCustomerAddress.php @@ -0,0 +1,60 @@ +addressRepository = $addressRepository; + } + + /** + * Delete customer address + * + * @param AddressInterface $address + * @return void + * @throws GraphQlInputException + */ + public function execute(AddressInterface $address): void + { + if ($address->isDefaultBilling()) { + throw new GraphQlInputException( + __('Customer Address %1 is set as default billing address and can not be deleted', [$address->getId()]) + ); + } + if ($address->isDefaultShipping()) { + throw new GraphQlInputException( + __('Customer Address %1 is set as default shipping address and can not be deleted', [$address->getId()]) + ); + } + + try { + $this->addressRepository->delete($address); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php new file mode 100644 index 0000000000000..a4649bccc02e8 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php @@ -0,0 +1,130 @@ +serviceOutputProcessor = $serviceOutputProcessor; + $this->jsonSerializer = $jsonSerializer; + $this->customerResourceModel = $customerResourceModel; + $this->customerFactory = $customerFactory; + } + + /** + * Curate shipping and billing default options + * + * @param array $address + * @param AddressInterface $addressObject + * @return array + */ + private function curateAddressDefaultValues(array $address, AddressInterface $addressObject) : array + { + $customerModel = $this->customerFactory->create(); + $this->customerResourceModel->load($customerModel, $addressObject->getCustomerId()); + $address[CustomerInterface::DEFAULT_BILLING] = + ($customerModel->getDefaultBillingAddress() + && $addressObject->getId() == $customerModel->getDefaultBillingAddress()->getId()); + $address[CustomerInterface::DEFAULT_SHIPPING] = + ($customerModel->getDefaultShippingAddress() + && $addressObject->getId() == $customerModel->getDefaultShippingAddress()->getId()); + return $address; + } + + /** + * Transform single customer address data from object to in array format + * + * @param AddressInterface $address + * @return array + */ + public function execute(AddressInterface $address): array + { + $addressData = $this->serviceOutputProcessor->process( + $address, + AddressRepositoryInterface::class, + 'getById' + ); + $addressData = $this->curateAddressDefaultValues($addressData, $address); + + if (isset($addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY])) { + $addressData = array_merge( + $addressData, + $addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY] + ); + } + $customAttributes = []; + if (isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { + foreach ($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) { + $isArray = false; + if (is_array($attribute['value'])) { + $isArray = true; + foreach ($attribute['value'] as $attributeValue) { + if (is_array($attributeValue)) { + $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( + $attribute['value'] + ); + continue; + } + $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); + continue; + } + } + if ($isArray) { + continue; + } + $customAttributes[$attribute['attribute_code']] = $attribute['value']; + } + } + $addressData = array_merge($addressData, $customAttributes); + + return $addressData; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddress.php new file mode 100644 index 0000000000000..7258f2e726fd7 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddress.php @@ -0,0 +1,68 @@ +addressRepository = $addressRepository; + } + + /** + * Get customer address + * + * @param int $addressId + * @param int $customerId + * @return AddressInterface + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + * @throws GraphQlAuthorizationException + */ + public function execute(int $addressId, int $customerId): AddressInterface + { + try { + $customerAddress = $this->addressRepository->getById($addressId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Could not find a address with ID "%address_id"', ['address_id' => $addressId]) + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + if ((int)$customerAddress->getCustomerId() !== $customerId) { + throw new GraphQlAuthorizationException( + __( + 'Current customer does not have permission to address with ID "%address_id"', + ['address_id' => $addressId] + ) + ); + } + return $customerAddress; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddressForUser.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddressForUser.php deleted file mode 100644 index f7323402a6c62..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddressForUser.php +++ /dev/null @@ -1,61 +0,0 @@ -addressRepository = $addressRepository; - } - - /** - * Get customer address for user - * - * @param int $addressId - * @param int $userId - * @return AddressInterface - * @throws GraphQlAuthorizationException - * @throws GraphQlNoSuchEntityException - */ - public function execute(int $addressId, int $userId): AddressInterface - { - try { - /** @var AddressInterface $address */ - $address = $this->addressRepository->getById($addressId); - } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException( - __('Address id %1 does not exist.', [$addressId]) - ); - } - - if ($address->getCustomerId() != $userId) { - throw new GraphQlAuthorizationException( - __('Current customer does not have permission to address id %1', [$addressId]) - ); - } - return $address; - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php new file mode 100644 index 0000000000000..65745a20bc8eb --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php @@ -0,0 +1,111 @@ +getAllowedAddressAttributes = $getAllowedAddressAttributes; + $this->addressRepository = $addressRepository; + $this->dataObjectHelper = $dataObjectHelper; + $this->restrictedKeys = $restrictedKeys; + } + + /** + * Update customer address + * + * @param AddressInterface $address + * @param array $data + * @return void + * @throws GraphQlInputException + */ + public function execute(AddressInterface $address, array $data): void + { + $this->validateData($data); + + $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); + $this->dataObjectHelper->populateWithArray($address, $filteredData, AddressInterface::class); + + if (isset($data['region']['region_id'])) { + $address->setRegionId($address->getRegion()->getRegionId()); + } + + try { + $this->addressRepository->save($address); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } + + /** + * Validate customer address update data + * + * @param array $addressData + * @return void + * @throws GraphQlInputException + */ + public function validateData(array $addressData): void + { + $attributes = $this->getAllowedAddressAttributes->execute(); + $errorInput = []; + + foreach ($attributes as $attributeName => $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (isset($addressData[$attributeName]) && empty($addressData[$attributeName])) + ) { + $errorInput[] = $attributeName; + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php deleted file mode 100644 index 030fc47d19e81..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerAccount.php +++ /dev/null @@ -1,103 +0,0 @@ -authentication = $authentication; - $this->customerRepository = $customerRepository; - $this->accountManagement = $accountManagement; - } - - /** - * Check customer account - * - * @param int|null $customerId - * @param int|null $customerType - * @return void - * @throws GraphQlAuthorizationException - * @throws GraphQlNoSuchEntityException - * @throws GraphQlAuthenticationException - */ - public function execute(?int $customerId, ?int $customerType): void - { - if (true === $this->isCustomerGuest($customerId, $customerType)) { - throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); - } - - try { - $this->customerRepository->getById($customerId); - } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException( - __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]), - $e - ); - } - - if (true === $this->authentication->isLocked($customerId)) { - throw new GraphQlAuthenticationException(__('The account is locked.')); - } - - $confirmationStatus = $this->accountManagement->getConfirmationStatus($customerId); - if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { - throw new GraphQlAuthenticationException(__("This account isn't confirmed. Verify and try again.")); - } - } - - /** - * Checking if current customer is guest - * - * @param int|null $customerId - * @param int|null $customerType - * @return bool - */ - private function isCustomerGuest(?int $customerId, ?int $customerType): bool - { - if (null === $customerId || null === $customerType) { - return true; - } - return 0 === (int)$customerId || (int)$customerType === UserContextInterface::USER_TYPE_GUEST; - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php index f3c03e5fc18aa..3cc831e1ca40e 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php @@ -9,7 +9,12 @@ use Magento\Customer\Model\AuthenticationInterface; use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\State\UserLockedException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** * Check customer password @@ -36,15 +41,21 @@ public function __construct( * @param string $password * @param int $customerId * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException */ public function execute(string $password, int $customerId) { try { $this->authentication->authenticate($customerId, $password); } catch (InvalidEmailOrPasswordException $e) { - throw new GraphQlAuthenticationException( - __('The password doesn\'t match this account. Verify the password and try again.') - ); + throw new GraphQlAuthenticationException(__($e->getMessage()), $e); + } catch (UserLockedException $e) { + throw new GraphQlAuthenticationException(__($e->getMessage()), $e); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php new file mode 100644 index 0000000000000..b7b66df042467 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php @@ -0,0 +1,112 @@ +dataObjectHelper = $dataObjectHelper; + $this->customerFactory = $customerFactory; + $this->accountManagement = $accountManagement; + $this->storeManager = $storeManager; + $this->changeSubscriptionStatus = $changeSubscriptionStatus; + } + + /** + * Creates new customer account + * + * @param array $data + * @return CustomerInterface + * @throws GraphQlInputException + */ + public function execute(array $data): CustomerInterface + { + try { + $customer = $this->createAccount($data); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if (isset($data['is_subscribed'])) { + $this->changeSubscriptionStatus->execute((int)$customer->getId(), (bool)$data['is_subscribed']); + } + return $customer; + } + + /** + * Create account + * + * @param array $data + * @return CustomerInterface + * @throws LocalizedException + */ + private function createAccount(array $data): CustomerInterface + { + $customerDataObject = $this->customerFactory->create(); + $this->dataObjectHelper->populateWithArray( + $customerDataObject, + $data, + CustomerInterface::class + ); + $store = $this->storeManager->getStore(); + $customerDataObject->setWebsiteId($store->getWebsiteId()); + $customerDataObject->setStoreId($store->getId()); + + $password = array_key_exists('password', $data) ? $data['password'] : null; + return $this->accountManagement->createAccount($customerDataObject, $password); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CustomerDataProvider.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CustomerDataProvider.php deleted file mode 100644 index c8382593eab23..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CustomerDataProvider.php +++ /dev/null @@ -1,136 +0,0 @@ -customerRepository = $customerRepository; - $this->serviceOutputProcessor = $serviceOutputProcessor; - $this->serializer = $serializer; - } - - /** - * Get customer data by Id or empty array - * - * @param int $customerId - * @return array - * @throws NoSuchEntityException|LocalizedException - */ - public function getCustomerById(int $customerId): array - { - try { - $customer = $this->customerRepository->getById($customerId); - } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException( - __('Customer id "%customer_id" does not exist.', ['customer_id' => $customerId]), - $e - ); - } - return $this->processCustomer($customer); - } - - /** - * Curate default shipping and default billing keys - * - * @param array $arrayAddress - * @return array - */ - private function curateAddressData(array $arrayAddress) : array - { - foreach ($arrayAddress as $key => $address) { - if (!isset($address['default_shipping'])) { - $arrayAddress[$key]['default_shipping'] = false; - } - if (!isset($address['default_billing'])) { - $arrayAddress[$key]['default_billing'] = false; - } - } - return $arrayAddress; - } - - /** - * Transform single customer data from object to in array format - * - * @param CustomerInterface $customer - * @return array - */ - private function processCustomer(CustomerInterface $customer): array - { - $customerData = $this->serviceOutputProcessor->process( - $customer, - CustomerRepositoryInterface::class, - 'get' - ); - $customerData['addresses'] = $this->curateAddressData($customerData['addresses']); - if (isset($customerData['extension_attributes'])) { - $customerData = array_merge($customerData, $customerData['extension_attributes']); - } - $customAttributes = []; - if (isset($customerData['custom_attributes'])) { - foreach ($customerData['custom_attributes'] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->serializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } - } - $customerData = array_merge($customerData, $customAttributes); - - return $customerData; - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php new file mode 100644 index 0000000000000..de37482aca056 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php @@ -0,0 +1,108 @@ +serviceOutputProcessor = $serviceOutputProcessor; + $this->serializer = $serializer; + } + + /** + * Curate default shipping and default billing keys + * + * @param array $arrayAddress + * @return array + */ + private function curateAddressData(array $arrayAddress): array + { + foreach ($arrayAddress as $key => $address) { + if (!isset($address['default_shipping'])) { + $arrayAddress[$key]['default_shipping'] = false; + } + if (!isset($address['default_billing'])) { + $arrayAddress[$key]['default_billing'] = false; + } + } + return $arrayAddress; + } + + /** + * Transform single customer data from object to in array format + * + * @param CustomerInterface $customer + * @return array + * @throws LocalizedException + */ + public function execute(CustomerInterface $customer): array + { + $customerData = $this->serviceOutputProcessor->process( + $customer, + CustomerRepositoryInterface::class, + 'get' + ); + $customerData['addresses'] = $this->curateAddressData($customerData['addresses']); + if (isset($customerData['extension_attributes'])) { + $customerData = array_merge($customerData, $customerData['extension_attributes']); + } + $customAttributes = []; + if (isset($customerData['custom_attributes'])) { + foreach ($customerData['custom_attributes'] as $attribute) { + $isArray = false; + if (is_array($attribute['value'])) { + $isArray = true; + foreach ($attribute['value'] as $attributeValue) { + if (is_array($attributeValue)) { + $customAttributes[$attribute['attribute_code']] = $this->serializer->serialize( + $attribute['value'] + ); + continue; + } + $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); + continue; + } + } + if ($isArray) { + continue; + } + $customAttributes[$attribute['attribute_code']] = $attribute['value']; + } + } + $customerData = array_merge($customerData, $customAttributes); + + $customerData['model'] = $customer; + return $customerData; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php new file mode 100644 index 0000000000000..8bd5c9157493c --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php @@ -0,0 +1,118 @@ +authentication = $authentication; + $this->customerRepository = $customerRepository; + $this->accountManagement = $accountManagement; + } + + /** + * Get customer + * + * @param ContextInterface $context + * @return CustomerInterface + * @throws GraphQlAuthenticationException + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context): CustomerInterface + { + $currentUserId = $context->getUserId(); + $currentUserType = $context->getUserType(); + + if (true === $this->isUserGuest($currentUserId, $currentUserType)) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + try { + $customer = $this->customerRepository->getById($currentUserId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Customer with id "%customer_id" does not exist.', ['customer_id' => $currentUserId]), + $e + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if (true === $this->authentication->isLocked($currentUserId)) { + throw new GraphQlAuthenticationException(__('The account is locked.')); + } + + try { + $confirmationStatus = $this->accountManagement->getConfirmationStatus($currentUserId); + } 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 $customer; + } + + /** + * Checking if current customer is guest + * + * @param int|null $customerId + * @param int|null $customerType + * @return bool + */ + private function isUserGuest(?int $customerId, ?int $customerType): bool + { + if (null === $customerId || null === $customerType) { + return true; + } + return 0 === (int)$customerId || (int)$customerType === UserContextInterface::USER_TYPE_GUEST; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/SaveCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Customer/SaveCustomer.php new file mode 100644 index 0000000000000..1605c63b62f4c --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/SaveCustomer.php @@ -0,0 +1,58 @@ +customerRepository = $customerRepository; + } + + /** + * Save customer + * + * @param CustomerInterface $customer + * @throws GraphQlAlreadyExistsException + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + */ + public function execute(CustomerInterface $customer): void + { + try { + $this->customerRepository->save($customer); + } catch (AlreadyExistsException $e) { + throw new GraphQlAlreadyExistsException( + __('A customer with the same email address already exists in an associated website.'), + $e + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php new file mode 100644 index 0000000000000..8601d586b3c95 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php @@ -0,0 +1,108 @@ +saveCustomer = $saveCustomer; + $this->storeManager = $storeManager; + $this->checkCustomerPassword = $checkCustomerPassword; + $this->dataObjectHelper = $dataObjectHelper; + $this->restrictedKeys = $restrictedKeys; + $this->changeSubscriptionStatus = $changeSubscriptionStatus; + } + + /** + * Update customer account data + * + * @param CustomerInterface $customer + * @param array $data + * @return void + * @throws GraphQlAlreadyExistsException + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + */ + public function execute(CustomerInterface $customer, array $data): void + { + if (isset($data['email']) && $customer->getEmail() !== $data['email']) { + if (!isset($data['password']) || empty($data['password'])) { + throw new GraphQlInputException(__('Provide the current "password" to change "email".')); + } + + $this->checkCustomerPassword->execute($data['password'], (int)$customer->getId()); + $customer->setEmail($data['email']); + } + + $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); + $this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class); + + $customer->setStoreId($this->storeManager->getStore()->getId()); + + $this->saveCustomer->execute($customer); + + if (isset($data['is_subscribed'])) { + $this->changeSubscriptionStatus->execute((int)$customer->getId(), (bool)$data['is_subscribed']); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerData.php deleted file mode 100644 index 18510b872e64a..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerData.php +++ /dev/null @@ -1,94 +0,0 @@ -customerRepository = $customerRepository; - $this->storeManager = $storeManager; - $this->checkCustomerPassword = $checkCustomerPassword; - } - - /** - * Update account information - * - * @param int $customerId - * @param array $data - * @return void - * @throws GraphQlNoSuchEntityException - * @throws GraphQlInputException - * @throws GraphQlAlreadyExistsException - */ - public function execute(int $customerId, array $data): void - { - $customer = $this->customerRepository->getById($customerId); - - if (isset($data['firstname'])) { - $customer->setFirstname($data['firstname']); - } - - if (isset($data['lastname'])) { - $customer->setLastname($data['lastname']); - } - - if (isset($data['email']) && $customer->getEmail() !== $data['email']) { - if (!isset($data['password']) || empty($data['password'])) { - throw new GraphQlInputException(__('Provide the current "password" to change "email".')); - } - - $this->checkCustomerPassword->execute($data['password'], $customerId); - $customer->setEmail($data['email']); - } - - $customer->setStoreId($this->storeManager->getStore()->getId()); - - try { - $this->customerRepository->save($customer); - } catch (AlreadyExistsException $e) { - throw new GraphQlAlreadyExistsException( - __('A customer with the same email address already exists in an associated website.'), - $e - ); - } - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php index 78fa852a7ac59..317b7725b0265 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php @@ -9,8 +9,9 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\CustomerGraphQl\Model\Customer\CheckCustomerPassword; -use Magento\CustomerGraphQl\Model\Customer\CustomerDataProvider; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -22,9 +23,9 @@ class ChangePassword implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** * @var CheckCustomerPassword @@ -37,26 +38,26 @@ class ChangePassword implements ResolverInterface private $accountManagement; /** - * @var CustomerDataProvider + * @var ExtractCustomerData */ - private $customerDataProvider; + private $extractCustomerData; /** - * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomer $getCustomer * @param CheckCustomerPassword $checkCustomerPassword * @param AccountManagementInterface $accountManagement - * @param CustomerDataProvider $customerDataProvider + * @param ExtractCustomerData $extractCustomerData */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, + GetCustomer $getCustomer, CheckCustomerPassword $checkCustomerPassword, AccountManagementInterface $accountManagement, - CustomerDataProvider $customerDataProvider + ExtractCustomerData $extractCustomerData ) { - $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomer = $getCustomer; $this->checkCustomerPassword = $checkCustomerPassword; $this->accountManagement = $accountManagement; - $this->customerDataProvider = $customerDataProvider; + $this->extractCustomerData = $extractCustomerData; } /** @@ -69,24 +70,24 @@ public function resolve( array $value = null, array $args = null ) { - if (!isset($args['currentPassword'])) { + if (!isset($args['currentPassword']) || '' == trim($args['currentPassword'])) { throw new GraphQlInputException(__('Specify the "currentPassword" value.')); } - if (!isset($args['newPassword'])) { + if (!isset($args['newPassword']) || '' == trim($args['newPassword'])) { throw new GraphQlInputException(__('Specify the "newPassword" value.')); } - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); + $customer = $this->getCustomer->execute($context); + $customerId = (int)$customer->getId(); - $currentUserId = (int)$currentUserId; - $this->checkCustomerPassword->execute($args['currentPassword'], $currentUserId); + $this->checkCustomerPassword->execute($args['currentPassword'], $customerId); - $this->accountManagement->changePasswordById($currentUserId, $args['currentPassword'], $args['newPassword']); - - $data = $this->customerDataProvider->getCustomerById($currentUserId); - return $data; + try { + $this->accountManagement->changePasswordById($customerId, $args['currentPassword'], $args['newPassword']); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + return $this->extractCustomerData->execute($customer); } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php new file mode 100644 index 0000000000000..1ae22bcc12792 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php @@ -0,0 +1,67 @@ +extractCustomerData = $extractCustomerData; + $this->createCustomerAccount = $createCustomerAccount; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } + + $customer = $this->createCustomerAccount->execute($args['input']); + + $context->setUserId((int)$customer->getId()); + $context->setUserType(UserContextInterface::USER_TYPE_CUSTOMER); + + $data = $this->extractCustomerData->execute($customer); + return ['customer' => $data]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php index 823444e5a2d7d..fd8122de961ee 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php @@ -7,14 +7,9 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\Customer\Api\Data\AddressInterfaceFactory; -use Magento\Customer\Api\Data\AddressInterface; -use Magento\CustomerGraphQl\Model\Customer\Address\CustomerAddressCreateDataValidator; -use Magento\CustomerGraphQl\Model\Customer\Address\CustomerAddressDataProvider; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; -use Magento\Framework\Api\DataObjectHelper; -use Magento\Framework\Exception\InputException; +use Magento\CustomerGraphQl\Model\Customer\Address\CreateCustomerAddress as CreateCustomerAddressModel; +use Magento\CustomerGraphQl\Model\Customer\Address\ExtractCustomerAddressData; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; @@ -26,57 +21,33 @@ class CreateCustomerAddress implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** - * @var AddressRepositoryInterface + * @var CreateCustomerAddressModel */ - private $addressRepository; + private $createCustomerAddress; /** - * @var AddressInterfaceFactory + * @var ExtractCustomerAddressData */ - private $addressInterfaceFactory; + private $extractCustomerAddressData; /** - * @var CustomerAddressDataProvider - */ - private $customerAddressDataProvider; - - /** - * @var DataObjectHelper - */ - private $dataObjectHelper; - - /** - * @var CustomerAddressCreateDataValidator - */ - private $customerAddressCreateDataValidator; - - /** - * @param CheckCustomerAccount $checkCustomerAccount - * @param AddressRepositoryInterface $addressRepository - * @param AddressInterfaceFactory $addressInterfaceFactory - * @param CustomerAddressDataProvider $customerAddressDataProvider - * @param DataObjectHelper $dataObjectHelper - * @param CustomerAddressCreateDataValidator $customerAddressCreateDataValidator + * @param GetCustomer $getCustomer + * @param CreateCustomerAddressModel $createCustomerAddress + * @param ExtractCustomerAddressData $extractCustomerAddressData */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, - AddressRepositoryInterface $addressRepository, - AddressInterfaceFactory $addressInterfaceFactory, - CustomerAddressDataProvider $customerAddressDataProvider, - DataObjectHelper $dataObjectHelper, - CustomerAddressCreateDataValidator $customerAddressCreateDataValidator + GetCustomer $getCustomer, + CreateCustomerAddressModel $createCustomerAddress, + ExtractCustomerAddressData $extractCustomerAddressData ) { - $this->checkCustomerAccount = $checkCustomerAccount; - $this->addressRepository = $addressRepository; - $this->addressInterfaceFactory = $addressInterfaceFactory; - $this->customerAddressDataProvider = $customerAddressDataProvider; - $this->dataObjectHelper = $dataObjectHelper; - $this->customerAddressCreateDataValidator = $customerAddressCreateDataValidator; + $this->getCustomer = $getCustomer; + $this->createCustomerAddress = $createCustomerAddress; + $this->extractCustomerAddressData = $extractCustomerAddressData; } /** @@ -89,36 +60,13 @@ public function resolve( array $value = null, array $args = null ) { - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); - - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); - $this->customerAddressCreateDataValidator->validate($args['input']); - - $address = $this->createCustomerAddress((int)$currentUserId, $args['input']); - return $this->customerAddressDataProvider->getAddressData($address); - } + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } - /** - * Create customer address - * - * @param int $customerId - * @param array $addressData - * @return AddressInterface - * @throws GraphQlInputException - */ - private function createCustomerAddress(int $customerId, array $addressData) : AddressInterface - { - /** @var AddressInterface $address */ - $address = $this->addressInterfaceFactory->create(); - $this->dataObjectHelper->populateWithArray($address, $addressData, AddressInterface::class); - $address->setCustomerId($customerId); + $customer = $this->getCustomer->execute($context); - try { - $address = $this->addressRepository->save($address); - } catch (InputException $e) { - throw new GraphQlInputException(__($e->getMessage()), $e); - } - return $address; + $address = $this->createCustomerAddress->execute((int)$customer->getId(), $args['input']); + return $this->extractCustomerAddressData->execute($address); } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php index c3c78a1004da6..91048d4836c80 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php @@ -7,9 +7,9 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CustomerGraphQl\Model\Customer\CustomerDataProvider; +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -19,25 +19,25 @@ class Customer implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** - * @var CustomerDataProvider + * @var ExtractCustomerData */ - private $customerDataProvider; + private $extractCustomerData; /** - * @param CheckCustomerAccount $checkCustomerAccount - * @param CustomerDataProvider $customerDataProvider + * @param GetCustomer $getCustomer + * @param ExtractCustomerData $extractCustomerData */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, - CustomerDataProvider $customerDataProvider + GetCustomer $getCustomer, + ExtractCustomerData $extractCustomerData ) { - $this->checkCustomerAccount = $checkCustomerAccount; - $this->customerDataProvider = $customerDataProvider; + $this->getCustomer = $getCustomer; + $this->extractCustomerData = $extractCustomerData; } /** @@ -50,13 +50,8 @@ public function resolve( array $value = null, array $args = null ) { - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); + $customer = $this->getCustomer->execute($context); - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); - - $currentUserId = (int)$currentUserId; - $data = $this->customerDataProvider->getCustomerById($currentUserId); - return $data; + return $this->extractCustomerData->execute($customer); } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php new file mode 100644 index 0000000000000..e6e3887de423c --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php @@ -0,0 +1,71 @@ +getCustomer = $getCustomer; + $this->extractCustomerAddressData = $extractCustomerAddressData; + } + + /** + * @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 Customer $customer */ + $customer = $value['model']; + + $addressesData = []; + $addresses = $customer->getAddresses(); + + if (count($addresses)) { + foreach ($addresses as $address) { + $addressesData[] = $this->extractCustomerAddressData->execute($address); + } + } + return $addressesData; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php index 084857c84d5a4..08e82d930f268 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php @@ -7,14 +7,13 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\CustomerGraphQl\Model\Customer\Address\GetCustomerAddressForUser; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\CustomerGraphQl\Model\Customer\Address\DeleteCustomerAddress as DeleteCustomerAddressModel; +use Magento\CustomerGraphQl\Model\Customer\Address\GetCustomerAddress; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** * Customers address delete, used for GraphQL request processing. @@ -22,33 +21,33 @@ class DeleteCustomerAddress implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** - * @var AddressRepositoryInterface + * @var GetCustomerAddress */ - private $addressRepository; + private $getCustomerAddress; /** - * @var GetCustomerAddressForUser + * @var DeleteCustomerAddressModel */ - private $getCustomerAddressForUser; + private $deleteCustomerAddress; /** - * @param CheckCustomerAccount $checkCustomerAccount - * @param AddressRepositoryInterface $addressRepository - * @param GetCustomerAddressForUser $getCustomerAddressForUser + * @param GetCustomer $getCustomer + * @param GetCustomerAddress $getCustomerAddress + * @param DeleteCustomerAddressModel $deleteCustomerAddress */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, - AddressRepositoryInterface $addressRepository, - GetCustomerAddressForUser $getCustomerAddressForUser + GetCustomer $getCustomer, + GetCustomerAddress $getCustomerAddress, + DeleteCustomerAddressModel $deleteCustomerAddress ) { - $this->checkCustomerAccount = $checkCustomerAccount; - $this->addressRepository = $addressRepository; - $this->getCustomerAddressForUser = $getCustomerAddressForUser; + $this->getCustomer = $getCustomer; + $this->getCustomerAddress = $getCustomerAddress; + $this->deleteCustomerAddress = $deleteCustomerAddress; } /** @@ -61,36 +60,14 @@ public function resolve( array $value = null, array $args = null ) { - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); - - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); + if (!isset($args['id']) || empty($args['id'])) { + throw new GraphQlInputException(__('Address "id" value should be specified')); + } - return $this->deleteCustomerAddress((int)$currentUserId, (int)$args['id']); - } + $customer = $this->getCustomer->execute($context); + $address = $this->getCustomerAddress->execute((int)$args['id'], (int)$customer->getId()); - /** - * Delete customer address - * - * @param int $customerId - * @param int $addressId - * @return bool - * @throws GraphQlAuthorizationException - * @throws GraphQlNoSuchEntityException - */ - private function deleteCustomerAddress($customerId, $addressId) - { - $address = $this->getCustomerAddressForUser->execute($addressId, $customerId); - if ($address->isDefaultBilling()) { - throw new GraphQlAuthorizationException( - __('Customer Address %1 is set as default billing address and can not be deleted', [$addressId]) - ); - } - if ($address->isDefaultShipping()) { - throw new GraphQlAuthorizationException( - __('Customer Address %1 is set as default shipping address and can not be deleted', [$addressId]) - ); - } - return $this->addressRepository->delete($address); + $this->deleteCustomerAddress->execute($address); + return true; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsEmailAvailable.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsEmailAvailable.php new file mode 100644 index 0000000000000..ddf1aec275ece --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsEmailAvailable.php @@ -0,0 +1,60 @@ +accountManagement = $accountManagement; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['email']) || empty($args['email'])) { + throw new GraphQlInputException(__('"Email should be specified')); + } + + try { + $isEmailAvailable = $this->accountManagement->isEmailAvailable($args['email']); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + return [ + 'is_email_available' => $isEmailAvailable + ]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php index c0bd864b3ee09..fc5691d97cbfe 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php @@ -7,7 +7,7 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -19,9 +19,9 @@ class IsSubscribed implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** * @var SubscriberFactory @@ -29,14 +29,14 @@ class IsSubscribed implements ResolverInterface private $subscriberFactory; /** - * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomer $getCustomer * @param SubscriberFactory $subscriberFactory */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, + GetCustomer $getCustomer, SubscriberFactory $subscriberFactory ) { - $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomer = $getCustomer; $this->subscriberFactory = $subscriberFactory; } @@ -50,12 +50,9 @@ public function resolve( array $value = null, array $args = null ) { - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); + $customer = $this->getCustomer->execute($context); - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); - - $status = $this->subscriberFactory->create()->loadByCustomerId((int)$currentUserId)->isSubscribed(); + $status = $this->subscriberFactory->create()->loadByCustomerId((int)$customer->getId())->isSubscribed(); return (bool)$status; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php index 3301162dc0088..92779597e5afa 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php @@ -7,7 +7,7 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -19,9 +19,9 @@ class RevokeCustomerToken implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** * @var CustomerTokenServiceInterface @@ -29,14 +29,14 @@ class RevokeCustomerToken implements ResolverInterface private $customerTokenService; /** - * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomer $getCustomer * @param CustomerTokenServiceInterface $customerTokenService */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, + GetCustomer $getCustomer, CustomerTokenServiceInterface $customerTokenService ) { - $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomer = $getCustomer; $this->customerTokenService = $customerTokenService; } @@ -50,11 +50,8 @@ public function resolve( array $value = null, array $args = null ) { - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); + $customer = $this->getCustomer->execute($context); - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); - - return ['result' => $this->customerTokenService->revokeCustomerAccessToken((int)$currentUserId)]; + return ['result' => $this->customerTokenService->revokeCustomerAccessToken((int)$customer->getId())]; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php index 50760d2e2e31c..7e06a2a063b4b 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php @@ -7,12 +7,11 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\CustomerGraphQl\Model\Customer\ChangeSubscriptionStatus; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; -use Magento\CustomerGraphQl\Model\Customer\UpdateCustomerData; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\CustomerGraphQl\Model\Customer\UpdateCustomerAccount; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CustomerGraphQl\Model\Customer\CustomerDataProvider; +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -22,41 +21,33 @@ class UpdateCustomer implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** - * @var UpdateCustomerData + * @var UpdateCustomerAccount */ - private $updateCustomerData; + private $updateCustomerAccount; /** - * @var ChangeSubscriptionStatus + * @var ExtractCustomerData */ - private $changeSubscriptionStatus; + private $extractCustomerData; /** - * @var CustomerDataProvider - */ - private $customerDataProvider; - - /** - * @param CheckCustomerAccount $checkCustomerAccount - * @param UpdateCustomerData $updateCustomerData - * @param ChangeSubscriptionStatus $changeSubscriptionStatus - * @param CustomerDataProvider $customerDataProvider + * @param GetCustomer $getCustomer + * @param UpdateCustomerAccount $updateCustomerAccount + * @param ExtractCustomerData $extractCustomerData */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, - UpdateCustomerData $updateCustomerData, - ChangeSubscriptionStatus $changeSubscriptionStatus, - CustomerDataProvider $customerDataProvider + GetCustomer $getCustomer, + UpdateCustomerAccount $updateCustomerAccount, + ExtractCustomerData $extractCustomerData ) { - $this->checkCustomerAccount = $checkCustomerAccount; - $this->updateCustomerData = $updateCustomerData; - $this->changeSubscriptionStatus = $changeSubscriptionStatus; - $this->customerDataProvider = $customerDataProvider; + $this->getCustomer = $getCustomer; + $this->updateCustomerAccount = $updateCustomerAccount; + $this->extractCustomerData = $extractCustomerData; } /** @@ -73,19 +64,10 @@ public function resolve( throw new GraphQlInputException(__('"input" value should be specified')); } - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); - - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); - - $currentUserId = (int)$currentUserId; - $this->updateCustomerData->execute($currentUserId, $args['input']); - - if (isset($args['input']['is_subscribed'])) { - $this->changeSubscriptionStatus->execute($currentUserId, (bool)$args['input']['is_subscribed']); - } + $customer = $this->getCustomer->execute($context); + $this->updateCustomerAccount->execute($customer, $args['input']); - $data = $this->customerDataProvider->getCustomerById($currentUserId); + $data = $this->extractCustomerData->execute($customer); return ['customer' => $data]; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php index 7bae40e4cc5de..bf41b7ddd10c9 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php @@ -7,13 +7,11 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\Customer\Api\Data\AddressInterface; -use Magento\CustomerGraphQl\Model\Customer\Address\CustomerAddressDataProvider; -use Magento\CustomerGraphQl\Model\Customer\Address\CustomerAddressUpdateDataValidator; -use Magento\CustomerGraphQl\Model\Customer\Address\GetCustomerAddressForUser; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; -use Magento\Framework\Api\DataObjectHelper; +use Magento\CustomerGraphQl\Model\Customer\Address\ExtractCustomerAddressData; +use Magento\CustomerGraphQl\Model\Customer\Address\GetCustomerAddress; +use Magento\CustomerGraphQl\Model\Customer\Address\UpdateCustomerAddress as UpdateCustomerAddressModel; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -24,57 +22,41 @@ class UpdateCustomerAddress implements ResolverInterface { /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** - * @var AddressRepositoryInterface + * @var GetCustomerAddress */ - private $addressRepository; + private $getCustomerAddress; /** - * @var CustomerAddressDataProvider + * @var UpdateCustomerAddressModel */ - private $customerAddressDataProvider; + private $updateCustomerAddress; /** - * @var DataObjectHelper + * @var ExtractCustomerAddressData */ - private $dataObjectHelper; + private $extractCustomerAddressData; /** - * @var CustomerAddressUpdateDataValidator - */ - private $customerAddressUpdateDataValidator; - - /** - * @var GetCustomerAddressForUser - */ - private $getCustomerAddressForUser; - - /** - * @param CheckCustomerAccount $checkCustomerAccount - * @param AddressRepositoryInterface $addressRepository - * @param CustomerAddressDataProvider $customerAddressDataProvider - * @param DataObjectHelper $dataObjectHelper - * @param CustomerAddressUpdateDataValidator $customerAddressUpdateDataValidator - * @param GetCustomerAddressForUser $getCustomerAddressForUser + * @param GetCustomer $getCustomer + * @param GetCustomerAddress $getCustomerAddress + * @param UpdateCustomerAddressModel $updateCustomerAddress + * @param ExtractCustomerAddressData $extractCustomerAddressData */ public function __construct( - CheckCustomerAccount $checkCustomerAccount, - AddressRepositoryInterface $addressRepository, - CustomerAddressDataProvider $customerAddressDataProvider, - DataObjectHelper $dataObjectHelper, - CustomerAddressUpdateDataValidator $customerAddressUpdateDataValidator, - GetCustomerAddressForUser $getCustomerAddressForUser + GetCustomer $getCustomer, + GetCustomerAddress $getCustomerAddress, + UpdateCustomerAddressModel $updateCustomerAddress, + ExtractCustomerAddressData $extractCustomerAddressData ) { - $this->checkCustomerAccount = $checkCustomerAccount; - $this->addressRepository = $addressRepository; - $this->customerAddressDataProvider = $customerAddressDataProvider; - $this->dataObjectHelper = $dataObjectHelper; - $this->customerAddressUpdateDataValidator = $customerAddressUpdateDataValidator; - $this->getCustomerAddressForUser = $getCustomerAddressForUser; + $this->getCustomer = $getCustomer; + $this->getCustomerAddress = $getCustomerAddress; + $this->updateCustomerAddress = $updateCustomerAddress; + $this->extractCustomerAddressData = $extractCustomerAddressData; } /** @@ -87,28 +69,18 @@ public function resolve( array $value = null, array $args = null ) { - $currentUserId = $context->getUserId(); - $currentUserType = $context->getUserType(); + if (!isset($args['id']) || empty($args['id'])) { + throw new GraphQlInputException(__('Address "id" value should be specified')); + } - $this->checkCustomerAccount->execute($currentUserId, $currentUserType); - $this->customerAddressUpdateDataValidator->validate($args['input']); + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } - $address = $this->updateCustomerAddress((int)$currentUserId, (int)$args['id'], $args['input']); - return $this->customerAddressDataProvider->getAddressData($address); - } + $customer = $this->getCustomer->execute($context); + $address = $this->getCustomerAddress->execute((int)$args['id'], (int)$customer->getId()); - /** - * Update customer address - * - * @param int $customerId - * @param int $addressId - * @param array $addressData - * @return AddressInterface - */ - private function updateCustomerAddress(int $customerId, int $addressId, array $addressData) - { - $address = $this->getCustomerAddressForUser->execute($addressId, $customerId); - $this->dataObjectHelper->populateWithArray($address, $addressData, AddressInterface::class); - return $this->addressRepository->save($address); + $this->updateCustomerAddress->execute($address, $args['input']); + return $this->extractCustomerAddressData->execute($address); } } diff --git a/app/code/Magento/CustomerGraphQl/Test/Mftf/README.md b/app/code/Magento/CustomerGraphQl/Test/Mftf/README.md deleted file mode 100644 index ae023224f4d9b..0000000000000 --- a/app/code/Magento/CustomerGraphQl/Test/Mftf/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Customer Graph Ql Functional Tests - -The Functional Test Module for **Magento Customer Graph Ql** module. diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..f1bd3563fda3d --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -0,0 +1,16 @@ + + + + + + + Magento\Customer\Api\Data\CustomerInterface::EMAIL + + + + \ No newline at end of file diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index c92753b96225c..4e4fd1d0fa8ad 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -3,12 +3,16 @@ type Query { customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") @doc(description: "The customer query returns information about a customer account") + isEmailAvailable ( + email: String! @doc(description: "The new customer email") + ): IsEmailAvailableOutput @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\IsEmailAvailable") } type Mutation { generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GenerateCustomerToken") @doc(description:"Retrieve the customer token") changeCustomerPassword(currentPassword: String!, newPassword: String!): Customer @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ChangePassword") @doc(description:"Changes the password for the logged-in customer") - updateCustomer (input: UpdateCustomerInput!): UpdateCustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") + createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") @@ -50,15 +54,21 @@ type CustomerToken { token: String @doc(description: "The customer token") } -input UpdateCustomerInput { - firstname: String - lastname: String - email: String - password: String - is_subscribed: Boolean +input CustomerInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required") + dob: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") } -type UpdateCustomerOutput { +type CustomerOutput { customer: Customer! } @@ -81,7 +91,8 @@ type Customer @doc(description: "Customer defines the customer name and address taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") id: Int @doc(description: "The ID assigned to the customer") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") - addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") + addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") + gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") } type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ @@ -119,6 +130,10 @@ type CustomerAddressAttribute { value: String @doc(description: "Attribute value") } +type IsEmailAvailableOutput { + is_email_available: Boolean @doc(description: "Is email availabel value") +} + enum CountryCodeEnum @doc(description: "The list of countries codes") { AF @doc(description: "Afghanistan") AX @doc(description: "Åland Islands") @@ -365,4 +380,4 @@ enum CountryCodeEnum @doc(description: "The list of countries codes") { YE @doc(description: "Yemen") ZM @doc(description: "Zambia") ZW @doc(description: "Zimbabwe") -} \ No newline at end of file +} diff --git a/app/code/Magento/Deploy/Console/DeployStaticOptions.php b/app/code/Magento/Deploy/Console/DeployStaticOptions.php index 89cb3e4b30345..1c02d24f7e99c 100644 --- a/app/code/Magento/Deploy/Console/DeployStaticOptions.php +++ b/app/code/Magento/Deploy/Console/DeployStaticOptions.php @@ -6,6 +6,7 @@ namespace Magento\Deploy\Console; +use Magento\Deploy\Process\Queue; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -57,6 +58,11 @@ class DeployStaticOptions */ const JOBS_AMOUNT = 'jobs'; + /** + * Key for max execution time option + */ + const MAX_EXECUTION_TIME = 'max-execution-time'; + /** * Force run of static deploy */ @@ -150,6 +156,7 @@ public function getOptionsList() * Basic options * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ private function getBasicOptions() { @@ -216,6 +223,13 @@ private function getBasicOptions() 'Enable parallel processing using the specified number of jobs.', self::DEFAULT_JOBS_AMOUNT ), + new InputOption( + self::MAX_EXECUTION_TIME, + null, + InputOption::VALUE_OPTIONAL, + 'The maximum expected execution time of deployment static process (in seconds).', + Queue::DEFAULT_MAX_EXEC_TIME + ), new InputOption( self::SYMLINK_LOCALE, null, diff --git a/app/code/Magento/Deploy/Console/InputValidator.php b/app/code/Magento/Deploy/Console/InputValidator.php index b3301f60fec26..772410d58a461 100644 --- a/app/code/Magento/Deploy/Console/InputValidator.php +++ b/app/code/Magento/Deploy/Console/InputValidator.php @@ -9,6 +9,8 @@ use Magento\Deploy\Console\DeployStaticOptions as Options; use Magento\Framework\Validator\Locale; use Symfony\Component\Console\Input\InputInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validator\RegexFactory; /** * Command input arguments validator class @@ -55,14 +57,24 @@ class InputValidator */ private $localeValidator; + /** + * @var RegexFactory + */ + private $versionValidatorFactory; + /** * InputValidator constructor * * @param Locale $localeValidator + * @param RegexFactory $versionValidatorFactory */ - public function __construct(Locale $localeValidator) - { + public function __construct( + Locale $localeValidator, + ?RegexFactory $versionValidatorFactory = null + ) { $this->localeValidator = $localeValidator; + $this->versionValidatorFactory = $versionValidatorFactory ?: + ObjectManager::getInstance()->get(RegexFactory::class); } /** @@ -85,6 +97,9 @@ public function validate(InputInterface $input) $input->getArgument(Options::LANGUAGES_ARGUMENT) ?: ['all'], $input->getOption(Options::EXCLUDE_LANGUAGE) ); + $this->checkVersionInput( + $input->getOption(Options::CONTENT_VERSION) ?: '' + ); } /** @@ -147,4 +162,29 @@ private function checkLanguagesInput(array $languagesInclude, array $languagesEx } } } + + /** + * Version input checks + * + * @param string $contentVersion + * @throws \InvalidArgumentException + */ + private function checkVersionInput(string $contentVersion): void + { + if ($contentVersion) { + $versionValidator = $this->versionValidatorFactory->create( + [ + 'pattern' => '/^[A-Za-z0-9_.]+$/' + ] + ); + + if (!$versionValidator->isValid($contentVersion)) { + throw new \InvalidArgumentException( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots' + ); + } + } + } } diff --git a/app/code/Magento/Deploy/Model/Filesystem.php b/app/code/Magento/Deploy/Model/Filesystem.php index e7d4d31e6f144..bc19ee287858d 100644 --- a/app/code/Magento/Deploy/Model/Filesystem.php +++ b/app/code/Magento/Deploy/Model/Filesystem.php @@ -150,6 +150,8 @@ public function __construct( * * @param OutputInterface $output * @return void + * @throws LocalizedException + * @throws \Exception */ public function regenerateStatic( OutputInterface $output @@ -164,9 +166,12 @@ public function regenerateStatic( DirectoryList::STATIC_VIEW ] ); - + + $this->reinitCacheDirectories(); // Trigger code generation $this->compile($output); + + $this->reinitCacheDirectories(); // Trigger static assets compilation and deployment $this->deployStaticContent($output); } @@ -217,6 +222,7 @@ private function getAdminUserInterfaceLocales() * * @return array * @throws \InvalidArgumentException if unknown locale is provided by the store configuration + * @throws \Magento\Framework\Exception\FileSystemException */ private function getUsedLocales() { @@ -249,13 +255,6 @@ function ($locale) { protected function compile(OutputInterface $output) { $output->writeln('Starting compilation'); - $this->cleanupFilesystem( - [ - DirectoryList::CACHE, - DirectoryList::GENERATED_CODE, - DirectoryList::GENERATED_METADATA, - ] - ); $cmd = $this->functionCallPath . 'setup:di:compile'; /** @@ -279,6 +278,7 @@ protected function compile(OutputInterface $output) * * @param array $directoryCodeList * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ public function cleanupFilesystem($directoryCodeList) { @@ -322,6 +322,7 @@ public function cleanupFilesystem($directoryCodeList) * of inverse mask for setting access permissions to files and directories generated by Magento. * @link https://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html * @link https://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @throws \Magento\Framework\Exception\FileSystemException */ protected function changePermissions($directoryCodeList, $dirPermissions, $filePermissions) { @@ -347,6 +348,7 @@ protected function changePermissions($directoryCodeList, $dirPermissions, $fileP * of inverse mask for setting access permissions to files and directories generated by Magento. * @link https://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html * @link https://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @throws \Magento\Framework\Exception\FileSystemException */ public function lockStaticResources() { @@ -361,4 +363,15 @@ public function lockStaticResources() self::PERMISSIONS_FILE ); } + + /** + * Flush cache and restore the basic cache directories. + * + * @throws LocalizedException + */ + private function reinitCacheDirectories() + { + $command = $this->functionCallPath . 'cache:flush'; + $this->shell->execute($command); + } } diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php index c4c31b8ff77bf..fd7aad44e0a5b 100644 --- a/app/code/Magento/Deploy/Process/Queue.php +++ b/app/code/Magento/Deploy/Process/Queue.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Deploy\Process; use Magento\Deploy\Package\Package; @@ -125,6 +127,8 @@ public function __construct( } /** + * Adds deployment package. + * * @param Package $package * @param Package[] $dependencies * @return bool true on success @@ -140,6 +144,8 @@ public function add(Package $package, array $dependencies = []) } /** + * Returns packages array. + * * @return Package[] */ public function getPackages() @@ -162,6 +168,7 @@ public function process() $this->assertAndExecute($name, $packages, $packageJob); } $this->logger->info('.'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction sleep(3); foreach ($this->inProgress as $name => $package) { if ($this->isDeployed($package)) { @@ -209,6 +216,8 @@ private function assertAndExecute($name, array & $packages, array $packageJob) } /** + * Executes deployment package. + * * @param Package $package * @param string $name * @param array $packages @@ -244,6 +253,7 @@ private function awaitForAllProcesses() } } $this->logger->info('.'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction sleep(5); } if ($this->isCanBeParalleled()) { @@ -253,6 +263,8 @@ private function awaitForAllProcesses() } /** + * Checks if can be parallel. + * * @return bool */ private function isCanBeParalleled() @@ -261,9 +273,11 @@ private function isCanBeParalleled() } /** + * Executes the process. + * * @param Package $package * @return bool true on success for main process and exit for child process - * @SuppressWarnings(PHPMD.ExitExpression) + * @throws \RuntimeException */ private function execute(Package $package) { @@ -291,6 +305,7 @@ function () use ($package) { ); if ($this->isCanBeParalleled()) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $pid = pcntl_fork(); if ($pid === -1) { throw new \RuntimeException('Unable to fork a new process'); @@ -305,6 +320,7 @@ function () use ($package) { // process child process $this->inProgress = []; $this->deployPackageService->deploy($package, $this->options, true); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } else { $this->deployPackageService->deploy($package, $this->options); @@ -313,6 +329,8 @@ function () use ($package) { } /** + * Checks if package is deployed. + * * @param Package $package * @return bool */ @@ -320,11 +338,13 @@ private function isDeployed(Package $package) { if ($this->isCanBeParalleled()) { if ($package->getState() === null) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $pid = pcntl_waitpid($this->getPid($package), $status, WNOHANG); if ($pid === $this->getPid($package)) { $package->setState(Package::STATE_COMPLETED); unset($this->inProgress[$package->getPath()]); + // phpcs:ignore Magento2.Functions.DiscouragedFunction return pcntl_wexitstatus($status) === 0; } return false; @@ -334,17 +354,19 @@ private function isDeployed(Package $package) } /** + * Returns process ID or null if not found. + * * @param Package $package * @return int|null */ private function getPid(Package $package) { - return isset($this->processIds[$package->getPath()]) - ? $this->processIds[$package->getPath()] - : null; + return isset($this->processIds[$package->getPath()]) ?? null; } /** + * Checks timeout. + * * @return bool */ private function checkTimeout() @@ -357,11 +379,13 @@ private function checkTimeout() * * Protect against zombie process * + * @throws \RuntimeException * @return void */ public function __destruct() { foreach ($this->inProgress as $package) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (pcntl_waitpid($this->getPid($package), $status) === -1) { throw new \RuntimeException( 'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php index 66ec6e7418afd..8903997159914 100644 --- a/app/code/Magento/Deploy/Service/DeployStaticContent.php +++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php @@ -9,6 +9,7 @@ use Magento\Deploy\Process\QueueFactory; use Magento\Deploy\Console\DeployStaticOptions as Options; use Magento\Framework\App\View\Deployment\Version\StorageInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; @@ -16,6 +17,7 @@ * Main service for static content deployment * * Aggregates services to deploy static files, static files bundles, translations and minified templates + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DeployStaticContent { @@ -71,6 +73,7 @@ public function __construct( * Run deploy procedure * * @param array $options + * @throws LocalizedException * @return void */ public function deploy(array $options) @@ -85,24 +88,26 @@ public function deploy(array $options) return; } - $queue = $this->queueFactory->create( - [ - 'logger' => $this->logger, - 'options' => $options, - 'maxProcesses' => $this->getProcessesAmount($options), - 'deployPackageService' => $this->objectManager->create( - \Magento\Deploy\Service\DeployPackage::class, - [ - 'logger' => $this->logger - ] - ) - ] - ); + $queueOptions = [ + 'logger' => $this->logger, + 'options' => $options, + 'maxProcesses' => $this->getProcessesAmount($options), + 'deployPackageService' => $this->objectManager->create( + \Magento\Deploy\Service\DeployPackage::class, + [ + 'logger' => $this->logger + ] + ) + ]; + + if (isset($options[Options::MAX_EXECUTION_TIME])) { + $queueOptions['maxExecTime'] = (int)$options[Options::MAX_EXECUTION_TIME]; + } $deployStrategy = $this->deployStrategyFactory->create( $options[Options::STRATEGY], [ - 'queue' => $queue + 'queue' => $this->queueFactory->create($queueOptions) ] ); @@ -133,6 +138,8 @@ public function deploy(array $options) } /** + * Returns amount of parallel processes, returns zero if option wasn't set. + * * @param array $options * @return int */ @@ -142,6 +149,8 @@ private function getProcessesAmount(array $options) } /** + * Checks if need to refresh only version. + * * @param array $options * @return bool */ diff --git a/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php b/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php new file mode 100644 index 0000000000000..f3991222dfa8c --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php @@ -0,0 +1,210 @@ +objectManagerHelper = new ObjectManagerHelper($this); + + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex('/^[A-Za-z0-9_.]+$/'); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + + $localeObjectMock = $this->getMockBuilder(Locale::class)->setMethods(['isValid']) + ->disableOriginalConstructor() + ->getMock(); + + $localeObjectMock->expects($this->any())->method('isValid') + ->with('en_US') + ->will($this->returnValue(true)); + + $this->inputValidator = $this->objectManagerHelper->getObject( + InputValidator::class, + [ + 'localeValidator' => $localeObjectMock, + 'versionValidatorFactory' => $regexFactoryMock + ] + ); + } + + /** + * @throws \Zend_Validate_Exception + */ + public function testValidate() + { + $input = $this->getMockBuilder(ArrayInput::class) + ->disableOriginalConstructor() + ->setMethods(['getOption', 'getArgument']) + ->getMock(); + + $input->expects($this->atLeastOnce())->method('getArgument')->willReturn(['all']); + + $input->expects($this->atLeastOnce())->method('getOption') + ->willReturnMap( + [ + [Options::AREA, ['all']], + [Options::EXCLUDE_AREA, ['none']], + [Options::THEME, ['all']], + [Options::EXCLUDE_THEME, ['none']], + [Options::EXCLUDE_LANGUAGE, ['none']], + [Options::CONTENT_VERSION, '12345'] + ] + ); + + /** @noinspection PhpParamsInspection */ + $this->inputValidator->validate($input); + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkAreasInput() + */ + public function testCheckAreasInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['test']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['test']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--area (-a) and --exclude-area cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkThemesInput() + */ + public function testCheckThemesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['blank']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['luma']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--theme (-t) and --exclude-theme cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckLanguagesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, '', 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, 2, '', ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['all']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + '--language (-l) and --exclude-language cannot be used at the same time', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckVersionInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, 2, '', ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['none']), + new InputOption(Options::CONTENT_VERSION, null, 4, '', '/*!#') + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php b/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php index 00f70c6527a0d..d3ff594fa6121 100644 --- a/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php @@ -69,6 +69,9 @@ class FilesystemTest extends \PHPUnit\Framework\TestCase */ private $cmdPrefix; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); @@ -124,6 +127,9 @@ protected function setUp() $this->cmdPrefix = PHP_BINARY . ' -f ' . BP . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'magento '; } + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testRegenerateStatic() { $storeLocales = ['fr_FR', 'de_DE', 'nl_NL']; @@ -131,18 +137,16 @@ public function testRegenerateStatic() ->willReturn($storeLocales); $setupDiCompileCmd = $this->cmdPrefix . 'setup:di:compile'; - $this->shell->expects(self::at(0)) - ->method('execute') - ->with($setupDiCompileCmd); - $this->initAdminLocaleMock('en_US'); $usedLocales = ['fr_FR', 'de_DE', 'nl_NL', 'en_US']; + $cacheFlushCmd = $this->cmdPrefix . 'cache:flush'; $staticContentDeployCmd = $this->cmdPrefix . 'setup:static-content:deploy -f ' . implode(' ', $usedLocales); - $this->shell->expects(self::at(1)) + $this->shell + ->expects($this->exactly(4)) ->method('execute') - ->with($staticContentDeployCmd); + ->withConsecutive([$cacheFlushCmd], [$setupDiCompileCmd], [$cacheFlushCmd], [$staticContentDeployCmd]); $this->output->expects(self::at(0)) ->method('writeln') @@ -166,6 +170,7 @@ public function testRegenerateStatic() * @return void * @expectedException \InvalidArgumentException * @expectedExceptionMessage ;echo argument has invalid value, run info:language:list for list of available locales + * @throws \Magento\Framework\Exception\LocalizedException */ public function testGenerateStaticForNotAllowedStoreViewLocale() { @@ -184,6 +189,7 @@ public function testGenerateStaticForNotAllowedStoreViewLocale() * @return void * @expectedException \InvalidArgumentException * @expectedExceptionMessage ;echo argument has invalid value, run info:language:list for list of available locales + * @throws \Magento\Framework\Exception\LocalizedException */ public function testGenerateStaticForNotAllowedAdminLocale() { diff --git a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php index 75edc8cb4f6ee..396381960e544 100644 --- a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Deploy\Test\Unit\Service; +use Magento\Deploy\Console\DeployStaticOptions; use Magento\Deploy\Package\Package; use Magento\Deploy\Process\Queue; use Magento\Deploy\Service\Bundle; @@ -221,4 +222,35 @@ public function deployDataProvider() ] ]; } + + public function testMaxExecutionTimeOptionPassed() + { + $options = [ + DeployStaticOptions::MAX_EXECUTION_TIME => 100, + DeployStaticOptions::REFRESH_CONTENT_VERSION_ONLY => false, + DeployStaticOptions::JOBS_AMOUNT => 3, + DeployStaticOptions::STRATEGY => 'compact', + DeployStaticOptions::NO_JAVASCRIPT => true, + DeployStaticOptions::NO_HTML_MINIFY => true, + ]; + + $queueMock = $this->createMock(Queue::class); + $strategyMock = $this->createMock(CompactDeploy::class); + $this->queueFactory->expects($this->once()) + ->method('create') + ->with([ + 'logger' => $this->logger, + 'maxExecTime' => 100, + 'maxProcesses' => 3, + 'options' => $options, + 'deployPackageService' => null + ]) + ->willReturn($queueMock); + $this->deployStrategyFactory->expects($this->once()) + ->method('create') + ->with('compact', ['queue' => $queueMock]) + ->willReturn($strategyMock); + + $this->service->deploy($options); + } } diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index fd604aa1b397b..0c32baebf12df 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -71,18 +71,4 @@ - - - - - - 0 - - - - - - - - diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php index 9bfee42fa6a83..ba98524bb665e 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php @@ -5,10 +5,10 @@ */ namespace Magento\Developer\Model\Logger\Handler; +use Magento\Config\Setup\ConfigOptionsList; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\State; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\DeploymentConfig; /** @@ -37,6 +37,7 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug * @param ScopeConfigInterface $scopeConfig * @param DeploymentConfig $deploymentConfig * @param string $filePath + * @throws \Exception */ public function __construct( DriverInterface $filesystem, @@ -53,16 +54,32 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function isHandling(array $record) { if ($this->deploymentConfig->isAvailable()) { return parent::isHandling($record) - && $this->scopeConfig->getValue('dev/debug/debug_logging', ScopeInterface::SCOPE_STORE); + && $this->isLoggingEnabled(); } return parent::isHandling($record); } + + /** + * Check that logging functionality is enabled. + * + * @return bool + */ + private function isLoggingEnabled(): bool + { + $configValue = $this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING); + if ($configValue === null) { + $isEnabled = $this->state->getMode() !== State::MODE_PRODUCTION; + } else { + $isEnabled = (bool)$configValue; + } + return $isEnabled; + } } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php index 6dc276a696f6f..3f5ff58640313 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php @@ -7,22 +7,19 @@ namespace Magento\Developer\Model\Logger\Handler; +use Magento\Config\Setup\ConfigOptionsList; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; /** - * Enable/disable syslog logging based on the store config setting. + * Enable/disable syslog logging based on the deployment config setting. */ class Syslog extends \Magento\Framework\Logger\Handler\Syslog { - public const CONFIG_PATH = 'dev/syslog/syslog_logging'; - /** - * Scope config. - * - * @var ScopeConfigInterface + * @deprecated configuration value has been removed. */ - private $scopeConfig; + public const CONFIG_PATH = 'dev/syslog/syslog_logging'; /** * Deployment config. @@ -35,6 +32,7 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog * @param ScopeConfigInterface $scopeConfig Scope config * @param DeploymentConfig $deploymentConfig Deployment config * @param string $ident The string ident to be added to each message + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ScopeConfigInterface $scopeConfig, @@ -42,8 +40,6 @@ public function __construct( string $ident ) { parent::__construct($ident); - - $this->scopeConfig = $scopeConfig; $this->deploymentConfig = $deploymentConfig; } @@ -53,7 +49,18 @@ public function __construct( public function isHandling(array $record): bool { return parent::isHandling($record) - && $this->deploymentConfig->isAvailable() - && $this->scopeConfig->getValue(self::CONFIG_PATH); + && $this->deploymentConfig->isDbAvailable() + && $this->isLoggingEnabled(); + } + + /** + * Check that logging functionality is enabled. + * + * @return bool + */ + private function isLoggingEnabled(): bool + { + $configValue = $this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_SYSLOG_LOGGING); + return (bool)$configValue; } } diff --git a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php index 5cdcc6eb99af5..b752eaa111fa4 100644 --- a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php +++ b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php @@ -206,12 +206,15 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN foreach ($tableData[$elementType] as $tableElementData) { if ($tableElementData['type'] === 'foreign') { $referenceTable = $schema->getTableByName($tableElementData['referenceTable']); - $constraintName = $this->elementNameResolver->getFullFKName( - $table, - $table->getColumnByName($tableElementData['column']), - $referenceTable, - $referenceTable->getColumnByName($tableElementData['referenceColumn']) - ); + $column = $table->getColumnByName($tableElementData['column']); + $referenceColumn = $referenceTable->getColumnByName($tableElementData['referenceColumn']); + $constraintName = ($column !== false && $referenceColumn !== false) ? + $this->elementNameResolver->getFullFKName( + $table, + $column, + $referenceTable, + $referenceColumn + ) : null; } else { $constraintName = $this->elementNameResolver->getFullIndexName( $table, @@ -219,7 +222,9 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN $tableElementData['type'] ); } - $declaredStructure[$elementType][$constraintName] = true; + if ($constraintName) { + $declaredStructure[$elementType][$constraintName] = true; + } } } diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php index c116775d582bb..1c729c933ec1c 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php @@ -51,6 +51,9 @@ class DebugTest extends \PHPUnit\Framework\TestCase */ private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp() { $this->filesystemMock = $this->getMockBuilder(DriverInterface::class) @@ -80,70 +83,95 @@ protected function setUp() $this->model->setFormatter($this->formatterMock); } - public function testHandle() + /** + * @return void + */ + public function testHandleEnabledInDeveloperMode() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with('dev/debug/debug_logging', ScopeInterface::SCOPE_STORE, null) - ->willReturn(true); + $this->stateMock + ->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); $this->assertTrue($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); } - public function testHandleDisabledByProduction() + /** + * @return void + */ + public function testHandleEnabledInDefaultMode() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->once()) + $this->stateMock + ->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEFAULT); + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); - $this->assertFalse($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); + $this->assertTrue($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); } - public function testHandleDisabledByConfig() + /** + * @return void + */ + public function testHandleDisabledByProduction() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with('dev/debug/debug_logging', ScopeInterface::SCOPE_STORE, null) - ->willReturn(false); + $this->stateMock + ->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_PRODUCTION); + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); $this->assertFalse($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); } + /** + * @return void + */ public function testHandleDisabledByLevel() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->never()) + $this->stateMock + ->expects($this->never()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); $this->assertFalse($this->model->isHandling(['formatted' => false, 'level' => Logger::API])); } + /** + * @return void + */ public function testDeploymentConfigIsNotAvailable() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(false); - $this->stateMock->expects($this->never()) + $this->stateMock + ->expects($this->never()) ->method('getMode'); - $this->scopeConfigMock->expects($this->never()) + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); $this->assertTrue($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php index c744645b670b4..06c19d3f2e835 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php @@ -7,6 +7,7 @@ namespace Magento\Developer\Test\Unit\Model\Logger\Handler; +use Magento\Config\Setup\ConfigOptionsList; use Magento\Developer\Model\Logger\Handler\Syslog; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; @@ -34,6 +35,9 @@ class SyslogTest extends TestCase */ private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); @@ -46,35 +50,48 @@ protected function setUp() ); } + /** + * @return void + */ public function testIsHandling(): void { $record = [ 'level' => Monolog::DEBUG, ]; - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with(Syslog::CONFIG_PATH) - ->willReturn('1'); - $this->deploymentConfigMock->expects($this->once()) - ->method('isAvailable') + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('get') + ->with(ConfigOptionsList::CONFIG_PATH_SYSLOG_LOGGING) + ->willReturn(1); $this->assertTrue( $this->model->isHandling($record) ); } + /** + * @return void + */ public function testIsHandlingNotInstalled(): void { $record = [ 'level' => Monolog::DEBUG, ]; - $this->scopeConfigMock->expects($this->never()) + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); - $this->deploymentConfigMock->expects($this->once()) - ->method('isAvailable') + $this->deploymentConfigMock + ->expects($this->once()) + ->method('isDbAvailable') ->willReturn(false); $this->assertFalse( @@ -82,19 +99,27 @@ public function testIsHandlingNotInstalled(): void ); } + /** + * @return void + */ public function testIsHandlingDisabled(): void { $record = [ 'level' => Monolog::DEBUG, ]; - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with(Syslog::CONFIG_PATH) - ->willReturn('0'); - $this->deploymentConfigMock->expects($this->once()) - ->method('isAvailable') + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('get') + ->with(ConfigOptionsList::CONFIG_PATH_SYSLOG_LOGGING) + ->willReturn(0); $this->assertFalse( $this->model->isHandling($record) diff --git a/app/code/Magento/Developer/etc/adminhtml/system.xml b/app/code/Magento/Developer/etc/adminhtml/system.xml index 4ebc45f1a2ca2..c64abd6eae725 100644 --- a/app/code/Magento/Developer/etc/adminhtml/system.xml +++ b/app/code/Magento/Developer/etc/adminhtml/system.xml @@ -25,20 +25,6 @@ Magento\Developer\Model\Config\Backend\AllowedIps - - - - Not available in production mode. - Magento\Config\Model\Config\Source\Yesno - - - - - - - Magento\Config\Model\Config\Source\Yesno - -
diff --git a/app/code/Magento/Developer/i18n/de_DE.csv b/app/code/Magento/Developer/i18n/de_DE.csv deleted file mode 100644 index e9c858101b71c..0000000000000 --- a/app/code/Magento/Developer/i18n/de_DE.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/es_ES.csv b/app/code/Magento/Developer/i18n/es_ES.csv deleted file mode 100644 index e9c858101b71c..0000000000000 --- a/app/code/Magento/Developer/i18n/es_ES.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/fr_FR.csv b/app/code/Magento/Developer/i18n/fr_FR.csv deleted file mode 100644 index e9c858101b71c..0000000000000 --- a/app/code/Magento/Developer/i18n/fr_FR.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/nl_NL.csv b/app/code/Magento/Developer/i18n/nl_NL.csv deleted file mode 100644 index e9c858101b71c..0000000000000 --- a/app/code/Magento/Developer/i18n/nl_NL.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/pt_BR.csv b/app/code/Magento/Developer/i18n/pt_BR.csv deleted file mode 100644 index e9c858101b71c..0000000000000 --- a/app/code/Magento/Developer/i18n/pt_BR.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/zh_Hans_CN.csv b/app/code/Magento/Developer/i18n/zh_Hans_CN.csv deleted file mode 100644 index e9c858101b71c..0000000000000 --- a/app/code/Magento/Developer/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index d997db6ac1a3e..1ad8b79ad12f3 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -7,6 +7,7 @@ namespace Magento\Dhl\Model; use Magento\Catalog\Model\Product\Type; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Module\Dir; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Shipment; @@ -56,6 +57,13 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ const CODE = 'dhl'; + /** + * DHL service prefixes used for message reference + */ + private const SERVICE_PREFIX_QUOTE = 'QUOT'; + private const SERVICE_PREFIX_SHIPVAL = 'SHIP'; + private const SERVICE_PREFIX_TRACKING = 'TRCK'; + /** * Rate request data * @@ -206,6 +214,11 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ private $xmlValidator; + /** + * @var ProductMetadataInterface + */ + private $productMetadata; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory @@ -232,7 +245,8 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory * @param array $data - * @param \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator + * @param \Magento\Dhl\Model\Validator\XmlValidator|null $xmlValidator + * @param ProductMetadataInterface|null $productMetadata * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -261,7 +275,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory, array $data = [], - \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null + \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null, + ProductMetadataInterface $productMetadata = null ) { $this->readFactory = $readFactory; $this->_carrierHelper = $carrierHelper; @@ -295,6 +310,8 @@ public function __construct( } $this->xmlValidator = $xmlValidator ?: \Magento\Framework\App\ObjectManager::getInstance()->get(XmlValidator::class); + $this->productMetadata = $productMetadata + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductMetadataInterface::class); } /** @@ -983,18 +1000,29 @@ protected function _getQuotesFromServer($request) protected function _buildQuotesRequestXml() { $rawRequest = $this->_rawRequest; - $xmlStr = '' . - ''; + 'xsi:schemaLocation="http://www.dhl.com DCT-req_global-2.0.xsd"/>'; + $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $nodeGetQuote = $xml->addChild('GetQuote', '', ''); $nodeRequest = $nodeGetQuote->addChild('Request'); $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); - $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); - $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); + $nodeServiceHeader->addChild('MessageTime', $this->buildMessageTimestamp()); + $nodeServiceHeader->addChild( + 'MessageReference', + $this->buildMessageReference(self::SERVICE_PREFIX_QUOTE) + ); + $nodeServiceHeader->addChild('SiteID', (string) $this->getConfigData('id')); + $nodeServiceHeader->addChild('Password', (string) $this->getConfigData('password')); + + $nodeMetaData = $nodeRequest->addChild('MetaData'); + $nodeMetaData->addChild('SoftwareName', $this->buildSoftwareName()); + $nodeMetaData->addChild('SoftwareVersion', $this->buildSoftwareVersion()); $nodeFrom = $nodeGetQuote->addChild('From'); $nodeFrom->addChild('CountryCode', $rawRequest->getOrigCountryId()); @@ -1386,44 +1414,37 @@ protected function _doRequest() { $rawRequest = $this->_request; - $originRegion = $this->getCountryParams( - $this->_scopeConfig->getValue( - Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $this->getStore() - ) - )->getRegion(); - - if (!$originRegion) { - throw new \Magento\Framework\Exception\LocalizedException(__('Wrong Region')); - } - - if ($originRegion == 'AM') { - $originRegion = ''; - } - $xmlStr = '' . - ''; + ' xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd"' . + ' schemaVersion="6.0" />'; $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $nodeRequest = $xml->addChild('Request', '', ''); $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); + $nodeServiceHeader->addChild('MessageTime', $this->buildMessageTimestamp()); + // MessageReference must be 28 to 32 chars. + $nodeServiceHeader->addChild( + 'MessageReference', + $this->buildMessageReference(self::SERVICE_PREFIX_SHIPVAL) + ); $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); - if (!$originRegion) { - $xml->addChild('RequestedPickupTime', 'N', ''); - } - if ($originRegion !== 'AP') { - $xml->addChild('NewShipper', 'N', ''); + $originRegion = $this->getCountryParams( + $this->_scopeConfig->getValue( + Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $this->getStore() + ) + )->getRegion(); + if ($originRegion) { + $xml->addChild('RegionCode', $originRegion, ''); } + $xml->addChild('RequestedPickupTime', 'N', ''); + $xml->addChild('NewShipper', 'N', ''); $xml->addChild('LanguageCode', 'EN', ''); $xml->addChild('PiecesEnabled', 'Y', ''); @@ -1465,8 +1486,9 @@ protected function _doRequest() } $nodeConsignee->addChild('City', $rawRequest->getRecipientAddressCity()); - if ($originRegion !== 'AP') { - $nodeConsignee->addChild('Division', $rawRequest->getRecipientAddressStateOrProvinceCode()); + $recipientAddressStateOrProvinceCode = $rawRequest->getRecipientAddressStateOrProvinceCode(); + if ($recipientAddressStateOrProvinceCode) { + $nodeConsignee->addChild('Division', $recipientAddressStateOrProvinceCode); } $nodeConsignee->addChild('PostalCode', $rawRequest->getRecipientAddressPostalCode()); $nodeConsignee->addChild('CountryCode', $rawRequest->getRecipientAddressCountryCode()); @@ -1510,15 +1532,13 @@ protected function _doRequest() $nodeReference->addChild('ReferenceType', 'St'); /** Shipment Details */ - $this->_shipmentDetails($xml, $rawRequest, $originRegion); + $this->_shipmentDetails($xml, $rawRequest); /** Shipper */ $nodeShipper = $xml->addChild('Shipper', '', ''); $nodeShipper->addChild('ShipperID', (string)$this->getConfigData('account')); $nodeShipper->addChild('CompanyName', $rawRequest->getShipperContactCompanyName()); - if ($originRegion !== 'AP') { - $nodeShipper->addChild('RegisteredAccount', (string)$this->getConfigData('account')); - } + $nodeShipper->addChild('RegisteredAccount', (string)$this->getConfigData('account')); $address = $rawRequest->getShipperAddressStreet1() . ' ' . $rawRequest->getShipperAddressStreet2(); $address = $this->string->split($address, 35, false, true); @@ -1531,8 +1551,9 @@ protected function _doRequest() } $nodeShipper->addChild('City', $rawRequest->getShipperAddressCity()); - if ($originRegion !== 'AP') { - $nodeShipper->addChild('Division', $rawRequest->getShipperAddressStateOrProvinceCode()); + $shipperAddressStateOrProvinceCode = $rawRequest->getShipperAddressStateOrProvinceCode(); + if ($shipperAddressStateOrProvinceCode) { + $nodeShipper->addChild('Division', $shipperAddressStateOrProvinceCode); } $nodeShipper->addChild('PostalCode', $rawRequest->getShipperAddressPostalCode()); $nodeShipper->addChild('CountryCode', $rawRequest->getShipperAddressCountryCode()); @@ -1584,19 +1605,13 @@ protected function _doRequest() * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') { $nodeShipmentDetails = $xml->addChild('ShipmentDetails', '', ''); $nodeShipmentDetails->addChild('NumberOfPieces', count($rawRequest->getPackages())); - if ($originRegion) { - $nodeShipmentDetails->addChild( - 'CurrencyCode', - $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() - ); - } - $nodePieces = $nodeShipmentDetails->addChild('Pieces', '', ''); /* @@ -1615,18 +1630,12 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') } $nodePiece->addChild('PieceID', ++$i); $nodePiece->addChild('PackageType', $packageType); - $nodePiece->addChild('Weight', sprintf('%.1f', $package['params']['weight'])); + $nodePiece->addChild('Weight', sprintf('%.3f', $package['params']['weight'])); $params = $package['params']; if ($params['width'] && $params['length'] && $params['height']) { - if (!$originRegion) { - $nodePiece->addChild('Width', round($params['width'])); - $nodePiece->addChild('Height', round($params['height'])); - $nodePiece->addChild('Depth', round($params['length'])); - } else { - $nodePiece->addChild('Depth', round($params['length'])); - $nodePiece->addChild('Width', round($params['width'])); - $nodePiece->addChild('Height', round($params['height'])); - } + $nodePiece->addChild('Width', round($params['width'])); + $nodePiece->addChild('Height', round($params['height'])); + $nodePiece->addChild('Depth', round($params['length'])); } $content = []; foreach ($package['items'] as $item) { @@ -1635,58 +1644,40 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') $nodePiece->addChild('PieceContents', substr(implode(',', $content), 0, 34)); } - if (!$originRegion) { - $nodeShipmentDetails->addChild('Weight', sprintf('%.1f', $rawRequest->getPackageWeight())); - $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); - $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('Date', $this->_coreDate->date('Y-m-d')); - $nodeShipmentDetails->addChild('Contents', 'DHL Parcel'); - /** - * The DoorTo Element defines the type of delivery service that applies to the shipment. - * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to - * Door non-compliant) - */ - $nodeShipmentDetails->addChild('DoorTo', 'DD'); - $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } - $nodeShipmentDetails->addChild('PackageType', $packageType); - if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { - $nodeShipmentDetails->addChild('IsDutiable', 'Y'); - } - $nodeShipmentDetails->addChild( - 'CurrencyCode', - $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() - ); - } else { - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } - $nodeShipmentDetails->addChild('PackageType', $packageType); - $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); - $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); - $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); - - /** - * The DoorTo Element defines the type of delivery service that applies to the shipment. - * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to - * Door non-compliant) - */ - $nodeShipmentDetails->addChild('DoorTo', 'DD'); - $nodeShipmentDetails->addChild('Date', $this->_coreDate->date('Y-m-d')); - $nodeShipmentDetails->addChild('Contents', 'DHL Parcel TEST'); + $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); + $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); + $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild( + 'Date', + $this->_coreDate->date('Y-m-d', strtotime('now + 1day')) + ); + $nodeShipmentDetails->addChild('Contents', 'DHL Parcel'); + /** + * The DoorTo Element defines the type of delivery service that applies to the shipment. + * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to + * Door non-compliant) + */ + $nodeShipmentDetails->addChild('DoorTo', 'DD'); + $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); + if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { + $packageType = 'CP'; + } + $nodeShipmentDetails->addChild('PackageType', $packageType); + if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { + $nodeShipmentDetails->addChild('IsDutiable', 'Y'); } + $nodeShipmentDetails->addChild( + 'CurrencyCode', + $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() + ); } /** * Get tracking * * @param string|string[] $trackings - * @return Result|null + * @return \Magento\Shipping\Model\Tracking\Result|null */ public function getTracking($trackings) { @@ -1710,12 +1701,15 @@ protected function _getXMLTracking($trackings) ''; + ' xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd"' . + ' schemaVersion="1.0" />'; $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $requestNode = $xml->addChild('Request', '', ''); $serviceHeaderNode = $requestNode->addChild('ServiceHeader', '', ''); + $serviceHeaderNode->addChild('MessageTime', $this->buildMessageTimestamp()); + $serviceHeaderNode->addChild('MessageReference', $this->buildMessageReference(self::SERVICE_PREFIX_TRACKING)); $serviceHeaderNode->addChild('SiteID', (string)$this->getConfigData('id')); $serviceHeaderNode->addChild('Password', (string)$this->getConfigData('password')); @@ -1959,17 +1953,72 @@ protected function _prepareShippingLabelContent(\SimpleXMLElement $xml) } /** + * Verify if the shipment is dutiable + * * @param string $origCountryId * @param string $destCountryId * * @return bool */ - protected function isDutiable($origCountryId, $destCountryId) + protected function isDutiable($origCountryId, $destCountryId) : bool { $this->_checkDomesticStatus($origCountryId, $destCountryId); - return - self::DHL_CONTENT_TYPE_NON_DOC == $this->getConfigData('content_type') - || !$this->_isDomestic; + return !$this->_isDomestic; + } + + /** + * Builds a datetime string to be used as the MessageTime in accordance to the expected format. + * + * @param string|null $datetime + * @return string + */ + private function buildMessageTimestamp(string $datetime = null): string + { + return $this->_coreDate->date(\DATE_RFC3339, $datetime); + } + + /** + * Builds a string to be used as the MessageReference. + * + * @param string $servicePrefix + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function buildMessageReference(string $servicePrefix): string + { + $validPrefixes = [ + self::SERVICE_PREFIX_QUOTE, + self::SERVICE_PREFIX_SHIPVAL, + self::SERVICE_PREFIX_TRACKING + ]; + + if (!in_array($servicePrefix, $validPrefixes)) { + throw new \Magento\Framework\Exception\LocalizedException( + __("Invalid service prefix \"$servicePrefix\" provided while attempting to build MessageReference") + ); + } + + return str_replace('.', '', uniqid("MAGE_{$servicePrefix}_", true)); + } + + /** + * Builds a string to be used as the request SoftwareName. + * + * @return string + */ + private function buildSoftwareName(): string + { + return substr($this->productMetadata->getName(), 0, 30); + } + + /** + * Builds a string to be used as the request SoftwareVersion. + * + * @return string + */ + private function buildSoftwareVersion(): string + { + return substr($this->productMetadata->getVersion(), 0, 10); } } diff --git a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php index 96c76a17bc317..c3d82ef34a448 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php @@ -3,17 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Dhl\Test\Unit\Model; use Magento\Dhl\Model\Carrier; use Magento\Dhl\Model\Validator\XmlValidator; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Filesystem\Directory\Read; use Magento\Framework\Filesystem\Directory\ReadFactory; use Magento\Framework\HTTP\ZendClient; use Magento\Framework\HTTP\ZendClientFactory; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; @@ -32,7 +35,6 @@ use Magento\Store\Model\Website; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; -use Magento\Store\Model\ScopeInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -80,14 +82,19 @@ class CarrierTest extends \PHPUnit\Framework\TestCase private $xmlValidator; /** - * @var Request|MockObject + * @var LoggerInterface|MockObject */ - private $request; + private $logger; /** - * @var LoggerInterface|MockObject + * @var DateTime|MockObject */ - private $logger; + private $coreDateMock; + + /** + * @var ProductMetadataInterface + */ + private $productMetadataMock; /** * @inheritdoc @@ -96,35 +103,8 @@ protected function setUp() { $this->objectManager = new ObjectManager($this); - $this->request = $this->getMockBuilder(Request::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'getPackages', - 'getOrigCountryId', - 'setPackages', - 'setPackageWeight', - 'setPackageValue', - 'setValueWithDiscount', - 'setPackageCustomsValue', - 'setFreeMethodWeight', - 'getPackageWeight', - 'getFreeMethodWeight', - 'getOrderShipment', - ] - ) - ->getMock(); - $this->scope = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $xmlElFactory = $this->getXmlFactory(); - $rateFactory = $this->getRateFactory(); - $rateMethodFactory = $this->getRateMethodFactory(); - $httpClientFactory = $this->getHttpClientFactory(); - $configReader = $this->getConfigReader(); - $readFactory = $this->getReadFactory(); - $storeManager = $this->getStoreManager(); - $this->error = $this->getMockBuilder(Error::class) ->setMethods(['setCarrier', 'setCarrierTitle', 'setErrorMessage']) ->getMock(); @@ -135,31 +115,45 @@ protected function setUp() $this->errorFactory->method('create') ->willReturn($this->error); - $carrierHelper = $this->getCarrierHelper(); - $this->xmlValidator = $this->getMockBuilder(XmlValidator::class) ->disableOriginalConstructor() ->getMock(); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->coreDateMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->coreDateMock->method('date') + ->willReturn('currentTime'); + + $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productMetadataMock->method('getName') + ->willReturn('Software_Product_Name_30_Char_123456789'); + $this->productMetadataMock->method('getVersion') + ->willReturn('10Char_Ver123456789'); + $this->model = $this->objectManager->getObject( Carrier::class, [ 'scopeConfig' => $this->scope, - 'xmlSecurity' => new Security(), - 'logger' => $this->logger, - 'xmlElFactory' => $xmlElFactory, - 'rateFactory' => $rateFactory, 'rateErrorFactory' => $this->errorFactory, - 'rateMethodFactory' => $rateMethodFactory, - 'httpClientFactory' => $httpClientFactory, - 'readFactory' => $readFactory, - 'storeManager' => $storeManager, - 'configReader' => $configReader, - 'carrierHelper' => $carrierHelper, + 'logger' => $this->logger, + 'xmlSecurity' => new Security(), + 'xmlElFactory' => $this->getXmlFactory(), + 'rateFactory' => $this->getRateFactory(), + 'rateMethodFactory' => $this->getRateMethodFactory(), + 'carrierHelper' => $this->getCarrierHelper(), + 'configReader' => $this->getConfigReader(), + 'storeManager' => $this->getStoreManager(), + 'readFactory' => $this->getReadFactory(), + 'httpClientFactory' => $this->getHttpClientFactory(), 'data' => ['id' => 'dhl', 'store' => '1'], 'xmlValidator' => $this->xmlValidator, + 'coreDate' => $this->coreDateMock, + 'productMetadata' => $this->productMetadataMock ] ); } @@ -176,14 +170,14 @@ public function scopeConfigGetValue($path) 'carriers/dhl/shipment_days' => 'Mon,Tue,Wed,Thu,Fri,Sat', 'carriers/dhl/intl_shipment_days' => 'Mon,Tue,Wed,Thu,Fri,Sat', 'carriers/dhl/allowed_methods' => 'IE', - 'carriers/dhl/international_searvice' => 'IE', + 'carriers/dhl/international_service' => 'IE', 'carriers/dhl/gateway_url' => 'https://xmlpi-ea.dhl.com/XMLShippingServlet', 'carriers/dhl/id' => 'some ID', 'carriers/dhl/password' => 'some password', 'carriers/dhl/content_type' => 'N', 'carriers/dhl/nondoc_methods' => '1,3,4,8,P,Q,E,F,H,J,M,V,Y', 'carriers/dhl/showmethod' => 1, - 'carriers/dhl/title' => 'dhl Title', + 'carriers/dhl/title' => 'DHL Title', 'carriers/dhl/specificerrmsg' => 'dhl error message', 'carriers/dhl/unit_of_measure' => 'K', 'carriers/dhl/size' => '1', @@ -191,11 +185,16 @@ public function scopeConfigGetValue($path) 'carriers/dhl/width' => '1.6', 'carriers/dhl/depth' => '1.6', 'carriers/dhl/debug' => 1, - 'shipping/origin/country_id' => 'GB', + 'shipping/origin/country_id' => 'GB' ]; return isset($pathMap[$path]) ? $pathMap[$path] : null; } + /** + * Prepare shipping label content test + * + * @throws \ReflectionException + */ public function testPrepareShippingLabelContent() { $xml = simplexml_load_file( @@ -207,6 +206,8 @@ public function testPrepareShippingLabelContent() } /** + * Prepare shipping label content exception test + * * @dataProvider prepareShippingLabelContentExceptionDataProvider * @expectedException \Magento\Framework\Exception\LocalizedException * @expectedExceptionMessage Unable to retrieve shipping label @@ -217,6 +218,8 @@ public function testPrepareShippingLabelContentException(\SimpleXMLElement $xml) } /** + * Prepare shipping label content exception data provider + * * @return array */ public function prepareShippingLabelContentExceptionDataProvider() @@ -236,8 +239,11 @@ public function prepareShippingLabelContentExceptionDataProvider() } /** + * Invoke prepare shipping label content + * * @param \SimpleXMLElement $xml * @return \Magento\Framework\DataObject + * @throws \ReflectionException */ protected function _invokePrepareShippingLabelContent(\SimpleXMLElement $xml) { @@ -247,8 +253,14 @@ protected function _invokePrepareShippingLabelContent(\SimpleXMLElement $xml) return $method->invoke($model, $xml); } + /** + * Tests that valid rates are returned when sending a quotes request. + */ public function testCollectRates() { + $requestData = require __DIR__ . '/_files/dhl_quote_request_data.php'; + $responseXml = file_get_contents(__DIR__ . '/_files/dhl_quote_response.xml'); + $this->scope->method('getValue') ->willReturnCallback([$this, 'scopeConfigGetValue']); @@ -256,13 +268,14 @@ public function testCollectRates() ->willReturn(true); $this->httpResponse->method('getBody') - ->willReturn(file_get_contents(__DIR__ . '/_files/success_dhl_response_rates.xml')); + ->willReturn($responseXml); - /** @var RateRequest $request */ - $request = $this->objectManager->getObject( - RateRequest::class, - require __DIR__ . '/_files/rates_request_data_dhl.php' - ); + $this->coreDateMock->method('date') + ->willReturnCallback(function () { + return date(\DATE_RFC3339); + }); + + $request = $this->objectManager->getObject(RateRequest::class, $requestData); $reflectionClass = new \ReflectionObject($this->httpClient); $rawPostData = $reflectionClass->getProperty('raw_post_data'); @@ -272,13 +285,27 @@ public function testCollectRates() ->method('debug') ->with($this->stringContains('********')); - self::assertNotEmpty($this->model->collectRates($request)->getAllRates()); - self::assertContains('18.223', $rawPostData->getValue($this->httpClient)); - self::assertContains('0.630', $rawPostData->getValue($this->httpClient)); - self::assertContains('0.630', $rawPostData->getValue($this->httpClient)); - self::assertContains('0.630', $rawPostData->getValue($this->httpClient)); + $expectedRates = require __DIR__ . '/_files/dhl_quote_response_rates.php'; + $actualRates = $this->model->collectRates($request)->getAllRates(); + + self::assertEquals(count($expectedRates), count($actualRates)); + + foreach ($actualRates as $i => $actualRate) { + $actualRate = $actualRate->getData(); + unset($actualRate['method_title']); + self::assertEquals($expectedRates[$i], $actualRate); + } + + $requestXml = $rawPostData->getValue($this->httpClient); + self::assertContains('18.223', $requestXml); + self::assertContains('0.630', $requestXml); + self::assertContains('0.630', $requestXml); + self::assertContains('0.630', $requestXml); } + /** + * Tests that an error is returned when attempting to collect rates for an inactive shipping method. + */ public function testCollectRatesErrorMessage() { $this->scope->method('getValue') @@ -296,26 +323,81 @@ public function testCollectRatesErrorMessage() $this->assertSame($this->error, $this->model->collectRates($request)); } - public function testCollectRatesFail() + /** + * Test request to shipment sends valid xml values. + * + * @dataProvider requestToShipmentDataProvider + * @param string $origCountryId + * @param string $expectedRegionCode + * @param string $destCountryId + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \ReflectionException + */ + public function testRequestToShipment(string $origCountryId, string $expectedRegionCode, string $destCountryId) { - $this->scope->expects($this->once())->method('isSetFlag')->willReturn(true); + $scopeConfigValueMap = [ + ['carriers/dhl/account', 'store', null, '1234567890'], + ['carriers/dhl/gateway_url', 'store', null, 'https://xmlpi-ea.dhl.com/XMLShippingServlet'], + ['carriers/dhl/id', 'store', null, 'some ID'], + ['carriers/dhl/password', 'store', null, 'some password'], + ['carriers/dhl/content_type', 'store', null, 'N'], + ['carriers/dhl/nondoc_methods', 'store', null, '1,3,4,8,P,Q,E,F,H,J,M,V,Y'], + ['shipping/origin/country_id', 'store', null, $origCountryId], + ]; - $request = new RateRequest(); - $request->setPackageWeight(1); + $this->scope->method('getValue') + ->willReturnMap($scopeConfigValueMap); + + $this->httpResponse->method('getBody') + ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + + $request = $this->getRequest($origCountryId, $destCountryId); + + $this->logger->method('debug') + ->with($this->stringContains('********')); + + $result = $this->model->requestToShipment($request); - $this->assertFalse(false, $this->model->collectRates($request)); + $reflectionClass = new \ReflectionObject($this->httpClient); + $rawPostData = $reflectionClass->getProperty('raw_post_data'); + $rawPostData->setAccessible(true); + + $this->assertNotNull($result); + $requestXml = $rawPostData->getValue($this->httpClient); + $requestElement = new Element($requestXml); + + $messageReference = $requestElement->Request->ServiceHeader->MessageReference->__toString(); + $this->assertStringStartsWith('MAGE_SHIP_', $messageReference); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_SHIP_28TO32_Char_CHECKED'; + + $this->assertXmlStringEqualsXmlString( + $this->getExpectedRequestXml($origCountryId, $destCountryId, $expectedRegionCode)->asXML(), + $requestElement->asXML() + ); } /** - * Test request to shipment sends valid xml values. + * Prepare and retrieve request object + * + * @param string $origCountryId + * @param string $destCountryId + * @return Request|MockObject */ - public function testRequestToShipment() + private function getRequest(string $origCountryId, string $destCountryId) { - $this->scope->method('getValue') - ->willReturnCallback([$this, 'scopeConfigGetValue']); + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $order->method('getSubtotal') + ->willReturn('10.00'); - $this->httpResponse->method('getBody') - ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + $shipment = $this->getMockBuilder(Order\Shipment::class) + ->disableOriginalConstructor() + ->getMock(); + $shipment->method('getOrder') + ->willReturn($order); $packages = [ 'package' => [ @@ -337,52 +419,77 @@ public function testRequestToShipment() ], ]; - $order = $this->getMockBuilder(Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order->method('getSubtotal') - ->willReturn('10.00'); + $methods = [ + 'getPackages' => $packages, + 'getOrigCountryId' => $origCountryId, + 'getDestCountryId' => $destCountryId, + 'getShipperAddressCountryCode' => $origCountryId, + 'getRecipientAddressCountryCode' => $destCountryId, + 'setPackages' => null, + 'setPackageWeight' => null, + 'setPackageValue' => null, + 'setValueWithDiscount' => null, + 'setPackageCustomsValue' => null, + 'setFreeMethodWeight' => null, + 'getPackageWeight' => '0.454000000001', + 'getFreeMethodWeight' => '0.454000000001', + 'getOrderShipment' => $shipment, + ]; - $shipment = $this->getMockBuilder(Order\Shipment::class) + /** @var Request|MockObject $request */ + $request = $this->getMockBuilder(Request::class) ->disableOriginalConstructor() + ->setMethods(array_keys($methods)) ->getMock(); - $shipment->method('getOrder') - ->willReturn($order); - $this->request->method('getPackages') - ->willReturn($packages); - $this->request->method('getOrigCountryId') - ->willReturn('GB'); - $this->request->method('setPackages') - ->willReturnSelf(); - $this->request->method('setPackageWeight') - ->willReturnSelf(); - $this->request->method('setPackageValue') - ->willReturnSelf(); - $this->request->method('setValueWithDiscount') - ->willReturnSelf(); - $this->request->method('setPackageCustomsValue') - ->willReturnSelf(); - $this->request->method('setFreeMethodWeight') - ->willReturnSelf(); - $this->request->method('getPackageWeight') - ->willReturn('0.454000000001'); - $this->request->method('getFreeMethodWeight') - ->willReturn('0.454000000001'); - $this->request->method('getOrderShipment') - ->willReturn($shipment); + foreach ($methods as $method => $return) { + $return ? $request->method($method)->willReturn($return) : $request->method($method)->willReturnSelf(); + } - $this->logger->method('debug') - ->with($this->stringContains('********')); + return $request; + } - $result = $this->model->requestToShipment($this->request); + /** + * Prepare and retrieve expected request xml element + * + * @param string $origCountryId + * @param string $destCountryId + * @return Element + */ + private function getExpectedRequestXml(string $origCountryId, string $destCountryId, string $regionCode) + { + $requestXmlPath = $origCountryId == $destCountryId + ? '/_files/domestic_shipment_request.xml' + : '/_files/shipment_request.xml'; - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); + $expectedRequestElement = new Element(file_get_contents(__DIR__ . $requestXmlPath)); - $this->assertNotNull($result); - $this->assertContains('0.454', $rawPostData->getValue($this->httpClient)); + $expectedRequestElement->Consignee->CountryCode = $destCountryId; + $expectedRequestElement->Consignee->CountryName = $this->getCountryName($destCountryId); + + $expectedRequestElement->Shipper->CountryCode = $origCountryId; + $expectedRequestElement->Shipper->CountryName = $this->getCountryName($origCountryId); + + $expectedRequestElement->RegionCode = $regionCode; + + return $expectedRequestElement; + } + + /** + * Get Country Name by Country Code + * + * @param string $countryCode + * @return string + */ + private function getCountryName($countryCode) + { + $countryNames = [ + 'US' => 'United States of America', + 'SG' => 'Singapore', + 'GB' => 'United Kingdom', + 'DE' => 'Germany', + ]; + return $countryNames[$countryCode]; } /** @@ -394,89 +501,21 @@ public function requestToShipmentDataProvider() { return [ [ - 'GB' + 'GB', 'EU', 'US' + ], + [ + 'SG', 'AP', 'US' ], [ - null + 'DE', 'EU', 'DE' ] ]; } /** - * Test that shipping label request for origin country from AP region doesn't contain restricted fields. + * Get DHL products test * - * @return void - */ - public function testShippingLabelRequestForAsiaPacificRegion() - { - $this->scope->method('getValue') - ->willReturnMap( - [ - ['shipping/origin/country_id', ScopeInterface::SCOPE_STORE, null, 'SG'], - ['carriers/dhl/gateway_url', ScopeInterface::SCOPE_STORE, null, 'https://xmlpi-ea.dhl.com'], - ] - ); - - $this->httpResponse->method('getBody') - ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); - - $packages = [ - 'package' => [ - 'params' => [ - 'width' => '1', - 'length' => '1', - 'height' => '1', - 'dimension_units' => 'INCH', - 'weight_units' => 'POUND', - 'weight' => '0.45', - 'customs_value' => '10.00', - 'container' => Carrier::DHL_CONTENT_TYPE_NON_DOC, - ], - 'items' => [ - 'item1' => [ - 'name' => 'item_name', - ], - ], - ], - ]; - - $this->request->method('getPackages')->willReturn($packages); - $this->request->method('getOrigCountryId')->willReturn('SG'); - $this->request->method('setPackages')->willReturnSelf(); - $this->request->method('setPackageWeight')->willReturnSelf(); - $this->request->method('setPackageValue')->willReturnSelf(); - $this->request->method('setValueWithDiscount')->willReturnSelf(); - $this->request->method('setPackageCustomsValue')->willReturnSelf(); - - $result = $this->model->requestToShipment($this->request); - - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); - - $this->assertNotNull($result); - $requestXml = $rawPostData->getValue($this->httpClient); - - $this->assertNotContains( - 'NewShipper', - $requestXml, - 'NewShipper is restricted field for AP region' - ); - $this->assertNotContains( - 'Division', - $requestXml, - 'Division is restricted field for AP region' - ); - $this->assertNotContains( - 'RegisteredAccount', - $requestXml, - 'RegisteredAccount is restricted field for AP region' - ); - } - - /** * @dataProvider dhlProductsDataProvider - * * @param string $docType * @param array $products */ @@ -486,9 +525,11 @@ public function testGetDhlProducts(string $docType, array $products) } /** + * DHL products data provider + * * @return array */ - public function dhlProductsDataProvider() : array + public function dhlProductsDataProvider(): array { return [ 'doc' => [ @@ -537,6 +578,113 @@ public function dhlProductsDataProvider() : array ]; } + /** + * Tests that the built MessageReference string is of the appropriate format. + * + * @dataProvider buildMessageReferenceDataProvider + * @param $servicePrefix + * @throws \ReflectionException + */ + public function testBuildMessageReference($servicePrefix) + { + $method = new \ReflectionMethod($this->model, 'buildMessageReference'); + $method->setAccessible(true); + + $messageReference = $method->invoke($this->model, $servicePrefix); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + } + + /** + * Build message reference data provider + * + * @return array + */ + public function buildMessageReferenceDataProvider() + { + return [ + 'quote_prefix' => ['QUOT'], + 'shipval_prefix' => ['SHIP'], + 'tracking_prefix' => ['TRCK'] + ]; + } + + /** + * Tests that an exception is thrown when an invalid service prefix is provided. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid service prefix + */ + public function testBuildMessageReferenceInvalidPrefix() + { + $method = new \ReflectionMethod($this->model, 'buildMessageReference'); + $method->setAccessible(true); + + $method->invoke($this->model, 'INVALID'); + } + + /** + * Tests that the built software name string is of the appropriate format. + * + * @dataProvider buildSoftwareNameDataProvider + * @param $productName + * @throws \ReflectionException + */ + public function testBuildSoftwareName($productName) + { + $method = new \ReflectionMethod($this->model, 'buildSoftwareName'); + $method->setAccessible(true); + + $this->productMetadataMock->method('getName')->willReturn($productName); + + $softwareName = $method->invoke($this->model); + $this->assertLessThanOrEqual(30, strlen($softwareName)); + } + + /** + * Data provider for testBuildSoftwareName + * + * @return array + */ + public function buildSoftwareNameDataProvider() + { + return [ + 'valid_length' => ['Magento'], + 'exceeds_length' => ['Product_Name_Longer_Than_30_Char'] + ]; + } + + /** + * Tests that the built software version string is of the appropriate format. + * + * @dataProvider buildSoftwareVersionProvider + * @param $productVersion + * @throws \ReflectionException + */ + public function testBuildSoftwareVersion($productVersion) + { + $method = new \ReflectionMethod($this->model, 'buildSoftwareVersion'); + $method->setAccessible(true); + + $this->productMetadataMock->method('getVersion')->willReturn($productVersion); + + $softwareVersion = $method->invoke($this->model); + $this->assertLessThanOrEqual(10, strlen($softwareVersion)); + } + + /** + * Data provider for testBuildSoftwareVersion + * + * @return array + */ + public function buildSoftwareVersionProvider() + { + return [ + 'valid_length' => ['2.3.1'], + 'exceeds_length' => ['dev-MC-1000'] + ]; + } + /** * Creates mock for XML factory. * @@ -595,19 +743,25 @@ private function getRateMethodFactory(): MockObject ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $rateMethod = $this->getMockBuilder(Method::class) - ->disableOriginalConstructor() - ->setMethods(['setPrice']) - ->getMock(); - $rateMethod->method('setPrice') - ->willReturnSelf(); + $rateMethodFactory->method('create') - ->willReturn($rateMethod); + ->willReturnCallback(function () { + $rateMethod = $this->getMockBuilder(Method::class) + ->disableOriginalConstructor() + ->setMethods(['setPrice']) + ->getMock(); + $rateMethod->method('setPrice') + ->willReturnSelf(); + + return $rateMethod; + }); return $rateMethodFactory; } /** + * Get config reader + * * @return MockObject */ private function getConfigReader(): MockObject @@ -622,6 +776,8 @@ private function getConfigReader(): MockObject } /** + * Get read factory + * * @return MockObject */ private function getReadFactory(): MockObject @@ -640,6 +796,8 @@ private function getReadFactory(): MockObject } /** + * Get store manager + * * @return MockObject */ private function getStoreManager(): MockObject @@ -661,6 +819,8 @@ private function getStoreManager(): MockObject } /** + * Get carrier helper + * * @return CarrierHelper */ private function getCarrierHelper(): CarrierHelper @@ -679,6 +839,8 @@ private function getCarrierHelper(): CarrierHelper } /** + * Get HTTP client factory + * * @return MockObject */ private function getHttpClientFactory(): MockObject diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml index 3f28111f229d1..792465ce45942 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml @@ -1,4 +1,4 @@ - + + + + + + 2014-01-09T12:13:29.498+00:00 + EvgeniyUSA + + + + + + NUQ + NUQ + + + BER + BER + + E + E + EXPRESS 9:00 + EXPRESS 9:00 NONDOC + TD + N + Y + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 42.060 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT9H + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 3.790 + + 3.790 + USD + BILLC + + + 3.790 + USD + PULCL + + + 3.790 + USD + BASEC + + + 2014-01-09 + 45.850 + 0.000 + + USD + BILLC + 42.060 + 45.850 + 0.000 + 0.000 + + + USD + PULCL + 42.060 + 45.850 + 0.000 + 0.000 + + + USD + BASEC + 42.060 + 45.850 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + Q + Q + MEDICAL EXPRESS + MEDICAL EXPRESS + TD + Y + N + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 32.350 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT9H + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 2.910 + + 2.910 + USD + BILLC + + + 2.910 + USD + PULCL + + + 2.910 + USD + BASEC + + + 2014-01-09 + 35.260 + 0.000 + + USD + BILLC + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + PULCL + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + BASEC + 32.350 + 35.260 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + Y + Y + EXPRESS 12:00 + EXPRESS 12:00 NONDOC + TD + N + Y + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 34.290 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT12H + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 3.090 + + 3.090 + USD + BILLC + + + 3.090 + USD + PULCL + + + 3.090 + USD + BASEC + + + 2014-01-09 + 37.380 + 0.000 + + USD + BILLC + 34.290 + 37.380 + 0.000 + 0.000 + + + USD + PULCL + 34.290 + 37.380 + 0.000 + 0.000 + + + USD + BASEC + 34.290 + 37.380 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + 3 + 3 + B2C + EXPRESS WORLDWIDE (B2C) + TD + Y + N + 2014-01-09 + PT16H15M + PT15H15M + 1.000000 + 0 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT23H59M + 2.205 + LB + 4 + 1 + 2014-01-09 + 0.000 + 0.000 + + BILLC + 0 + 0.000 + 0.000 + 0.000 + + + USD + PULCL + 0 + 0.000 + 0.000 + 0.000 + + + USD + BASEC + 0 + 0.000 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + P + P + EXPRESS WORLDWIDE + EXPRESS WORLDWIDE NONDOC + TD + N + Y + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 32.350 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT23H59M + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 2.910 + + 2.910 + USD + BILLC + + + 2.910 + USD + PULCL + + + 2.910 + USD + BASEC + + + 2014-01-09 + 35.260 + 0.000 + + USD + BILLC + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + PULCL + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + BASEC + 32.350 + 35.260 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + + E + + E + EXPRESS 9:00 + EXPRESS 9:00 NONDOC + TD + N + Y + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + Q + + Q + MEDICAL EXPRESS + MEDICAL EXPRESS + TD + Y + N + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + Y + + Y + EXPRESS 12:00 + EXPRESS 12:00 NONDOC + TD + N + Y + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + 3 + + 3 + B2C + EXPRESS WORLDWIDE (B2C) + TD + Y + N + + + + P + + P + EXPRESS WORLDWIDE + EXPRESS WORLDWIDE NONDOC + TD + N + Y + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + + diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php new file mode 100644 index 0000000000000..ddd7b2e4f97c5 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php @@ -0,0 +1,31 @@ + 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 45.85, + 'method' => 'E' + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 35.26, + 'method' => 'Q' + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 37.38, + 'method' => 'Y' + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 35.26, + 'method' => 'P' + ] +]; diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml new file mode 100644 index 0000000000000..b71c2fa4a7dde --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml @@ -0,0 +1,88 @@ + + + + + + currentTime + MAGE_SHIP_28TO32_Char_CHECKED + some ID + some password + + + CHECKED + N + N + EN + Y + + 1234567890 + S + 1234567890 + S + 1234567890 + + + + + + + + + + + + + + + 1 + + + shipment reference + St + + + 1 + + + 1 + CP + 0.454 + 3 + 3 + 3 + item_name + + + 0.454 + K + + + currentTime + DHL Parcel + DD + C + CP + USD + + + 1234567890 + + 1234567890 + + + + + + + + + + + PDF + \ No newline at end of file diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php b/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php deleted file mode 100644 index 32e9c78886cef..0000000000000 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php +++ /dev/null @@ -1,49 +0,0 @@ - [ - 'dest_country_id' => 'DE', - 'dest_region_id' => '82', - 'dest_region_code' => 'BER', - 'dest_street' => 'Turmstraße 17', - 'dest_city' => 'Berlin', - 'dest_postcode' => '10559', - 'dest_postal' => '10559', - 'package_value' => '5', - 'package_value_with_discount' => '5', - 'package_weight' => '8.2657', - 'package_qty' => '1', - 'package_physical_value' => '5', - 'free_method_weight' => '5', - 'store_id' => '1', - 'website_id' => '1', - 'free_shipping' => '0', - 'limit_carrier' => null, - 'base_subtotal_incl_tax' => '5', - 'orig_country_id' => 'US', - 'country_id' => 'US', - 'region_id' => '12', - 'city' => 'Fremont', - 'postcode' => '94538', - 'dhl_id' => 'MAGEN_8501', - 'dhl_password' => 'QR2GO1U74X', - 'dhl_account' => '799909537', - 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', - 'girth' => null, - 'height' => null, - 'length' => null, - 'width' => null, - 'weight' => 1, - 'dhl_shipment_type' => 'P', - 'dhl_duitable' => 0, - 'dhl_duty_payment_type' => 'R', - 'dhl_content_desc' => 'Big Box', - 'limit_method' => 'IE', - 'ship_date' => '2014-01-09', - 'action' => 'RateEstimate', - 'all_items' => [], - ] -]; diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml new file mode 100644 index 0000000000000..d411041c96072 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml @@ -0,0 +1,93 @@ + + + + + + currentTime + MAGE_SHIP_28TO32_Char_CHECKED + some ID + some password + + + CHECKED + N + N + EN + Y + + 1234567890 + S + 1234567890 + S + 1234567890 + + + + + + + + + + + + + + + 1 + + + 10.00 + USD + + + shipment reference + St + + + 1 + + + 1 + CP + 0.454 + 3 + 3 + 3 + item_name + + + 0.454 + K + + + currentTime + DHL Parcel + DD + C + CP + Y + USD + + + 1234567890 + + 1234567890 + + + + + + + + + + + PDF + diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml deleted file mode 100644 index b529e86ef154c..0000000000000 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml +++ /dev/null @@ -1,478 +0,0 @@ - - - - - - - - 2014-01-09T12:13:29.498+00:00 - EvgeniyUSA - - - - - NUQ - NUQ - - - BER - BER - - - E - E - EXPRESS 9:00 - EXPRESS 9:00 NONDOC - TD - N - Y - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 42.060 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT9H - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 3.790 - - 3.790 - USD - BILLC - - - 3.790 - USD - PULCL - - - 3.790 - USD - BASEC - - - 2014-01-09 - 45.850 - 0.000 - - USD - BILLC - 42.060 - 45.850 - 0.000 - 0.000 - - - USD - PULCL - 42.060 - 45.850 - 0.000 - 0.000 - - - USD - BASEC - 42.060 - 45.850 - 0.000 - 0.000 - - - - Q - Q - MEDICAL EXPRESS - MEDICAL EXPRESS - TD - Y - N - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 32.350 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT9H - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 2.910 - - 2.910 - USD - BILLC - - - 2.910 - USD - PULCL - - - 2.910 - USD - BASEC - - - 2014-01-09 - 35.260 - 0.000 - - USD - BILLC - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - PULCL - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - BASEC - 32.350 - 35.260 - 0.000 - 0.000 - - - - Y - Y - EXPRESS 12:00 - EXPRESS 12:00 NONDOC - TD - N - Y - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 34.290 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT12H - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 3.090 - - 3.090 - USD - BILLC - - - 3.090 - USD - PULCL - - - 3.090 - USD - BASEC - - - 2014-01-09 - 37.380 - 0.000 - - USD - BILLC - 34.290 - 37.380 - 0.000 - 0.000 - - - USD - PULCL - 34.290 - 37.380 - 0.000 - 0.000 - - - USD - BASEC - 34.290 - 37.380 - 0.000 - 0.000 - - - - 3 - 3 - B2C - EXPRESS WORLDWIDE (B2C) - TD - Y - N - 2014-01-09 - PT16H15M - PT15H15M - 1.000000 - 0 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT23H59M - 2.205 - LB - 4 - 1 - 2014-01-09 - 0.000 - 0.000 - - BILLC - 0 - 0.000 - 0.000 - 0.000 - - - USD - PULCL - 0 - 0.000 - 0.000 - 0.000 - - - USD - BASEC - 0 - 0.000 - 0.000 - 0.000 - - - - P - P - EXPRESS WORLDWIDE - EXPRESS WORLDWIDE NONDOC - TD - N - Y - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 32.350 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT23H59M - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 2.910 - - 2.910 - USD - BILLC - - - 2.910 - USD - PULCL - - - 2.910 - USD - BASEC - - - 2014-01-09 - 35.260 - 0.000 - - USD - BILLC - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - PULCL - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - BASEC - 32.350 - 35.260 - 0.000 - 0.000 - - - - - - E - - E - EXPRESS 9:00 - EXPRESS 9:00 NONDOC - TD - N - Y - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - Q - - Q - MEDICAL EXPRESS - MEDICAL EXPRESS - TD - Y - N - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - Y - - Y - EXPRESS 12:00 - EXPRESS 12:00 NONDOC - TD - N - Y - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - 3 - - 3 - B2C - EXPRESS WORLDWIDE (B2C) - TD - Y - N - - - - P - - P - EXPRESS WORLDWIDE - EXPRESS WORLDWIDE NONDOC - TD - N - Y - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - - diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index 91ed6c6568a70..7ab37de2f3658 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -31,8 +31,9 @@ - - + + + Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic) Magento\Dhl\Model\Source\Contenttype @@ -81,18 +82,12 @@ - + Magento\Dhl\Model\Source\Method\Doc - - D - - + Magento\Dhl\Model\Source\Method\Nondoc - - N - diff --git a/app/code/Magento/Dhl/etc/countries.xml b/app/code/Magento/Dhl/etc/countries.xml index 48837dbefb576..792465ce45942 100644 --- a/app/code/Magento/Dhl/etc/countries.xml +++ b/app/code/Magento/Dhl/etc/countries.xml @@ -83,7 +83,7 @@ EUR KG CM - EA + EU Austria 1 @@ -132,7 +132,7 @@ EUR KG CM - EA + EU Belgium 1 @@ -146,7 +146,7 @@ BGN KG CM - EA + EU Bulgaria 1 @@ -257,7 +257,7 @@ CHF KG CM - EA + EU Switzerland @@ -331,7 +331,7 @@ CZK KG CM - EA + EU Czech Republic, The 1 @@ -339,7 +339,7 @@ EUR KG CM - EA + EU Germany 1 @@ -353,7 +353,7 @@ DKK KG CM - EA + EU Denmark 1 @@ -389,7 +389,7 @@ EEK KG CM - EA + EU Estonia 1 @@ -410,7 +410,7 @@ EUR KG CM - EA + EU Spain 1 @@ -424,7 +424,7 @@ EUR KG CM - EA + EU Finland 1 @@ -457,7 +457,7 @@ EUR KG CM - EA + EU France 1 @@ -471,7 +471,7 @@ GBP KG CM - EA + EU United Kingdom 1 @@ -549,7 +549,7 @@ EUR KG CM - EA + EU Greece 1 @@ -612,7 +612,7 @@ HUF KG CM - EA + EU Hungary 1 @@ -633,7 +633,7 @@ EUR KG CM - EA + EU Ireland, Republic Of 1 @@ -668,14 +668,14 @@ ISK KG CM - EA + EU Iceland EUR KG CM - EA + EU Italy 1 @@ -834,7 +834,7 @@ LTL KG CM - EA + EU Lithuania 1 @@ -842,7 +842,7 @@ EUR KG CM - EA + EU Luxembourg 1 @@ -850,7 +850,7 @@ LVL KG CM - EA + EU Latvia 1 @@ -1039,7 +1039,7 @@ EUR KG CM - EA + EU Netherlands, The 1 @@ -1047,7 +1047,7 @@ NOK KG CM - EA + EU Norway @@ -1127,7 +1127,7 @@ PLN KG CM - EA + EU Poland 1 @@ -1142,7 +1142,7 @@ EUR KG CM - EA + EU Portugal 1 @@ -1177,7 +1177,7 @@ RON KG CM - EA + EU Romania 1 @@ -1231,7 +1231,7 @@ SEK KG CM - EA + EU Sweden 1 @@ -1246,7 +1246,7 @@ EUR KG CM - EA + EU Slovenia 1 @@ -1254,7 +1254,7 @@ EUR KG CM - EA + EU Slovakia 1 diff --git a/app/code/Magento/Dhl/i18n/en_US.csv b/app/code/Magento/Dhl/i18n/en_US.csv index a5532c2cea963..0e4c7a8385b93 100644 --- a/app/code/Magento/Dhl/i18n/en_US.csv +++ b/app/code/Magento/Dhl/i18n/en_US.csv @@ -61,6 +61,7 @@ Title,Title Password,Password "Account Number","Account Number" "Content Type","Content Type" +"Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)","Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)" "Calculate Handling Fee","Calculate Handling Fee" "Handling Applied","Handling Applied" """Per Order"" allows a single handling fee for the entire order. ""Per Package"" allows an individual handling fee for each package.","""Per Order"" allows a single handling fee for the entire order. ""Per Package"" allows an individual handling fee for each package." diff --git a/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php b/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php index 97d22633af03c..c8c42b952042e 100644 --- a/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php +++ b/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php @@ -14,24 +14,14 @@ */ class WeightUnit implements \Magento\Framework\Option\ArrayInterface { - /** - * @var string - */ - const CODE_LBS = 'lbs'; - - /** - * @var string - */ - const CODE_KGS = 'kgs'; - /** * @inheritdoc */ public function toOptionArray() { return [ - ['value' => self::CODE_LBS, 'label' => __('lbs')], - ['value' => self::CODE_KGS, 'label' => __('kgs')] + ['value' => 'lbs', 'label' => __('lbs')], + ['value' => 'kgs', 'label' => __('kgs')] ]; } } diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index fdb561c224170..f7230df6e86ea 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -57,7 +57,7 @@ public function __construct( */ public function getConfigCurrencies(string $path) { - $result = $this->appState->getAreaCode() === Area::AREA_ADMINHTML + $result = in_array($this->appState->getAreaCode(), [Area::AREA_ADMINHTML, Area::AREA_CRONTAB]) ? $this->getConfigForAllStores($path) : $this->getConfigForCurrentStore($path); sort($result); diff --git a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php index f90a7b3b519b5..4ec34a3842fa2 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -205,6 +205,7 @@ public function getItemById($countryId) /** * Add filter by country code to collection. + * * $countryCode can be either array of country codes or string representing one country code. * $iso can be either array containing 'iso2', 'iso3' values or string with containing one of that values directly. * The collection will contain countries where at least one of country $iso fields matches $countryCode. @@ -297,7 +298,7 @@ public function toOptionArray($emptyLabel = ' ') } $options[] = $option; } - if ($emptyLabel !== false && count($options) > 0) { + if ($emptyLabel !== false && count($options) > 1) { array_unshift($options, ['value' => '', 'label' => $emptyLabel]); } @@ -326,7 +327,7 @@ private function addDefaultCountryToOptions(array &$options) foreach ($options as $key => $option) { if (isset($defaultCountry[$option['value']])) { - $options[$key]['is_default'] = $defaultCountry[$option['value']]; + $options[$key]['is_default'] = !empty($defaultCountry[$option['value']]); } } } diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index ffbcce11cb4f6..5339b0c9eb5bd 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -216,7 +216,7 @@ protected function _getRatesByCode($code, $toCurrencies = null) $connection = $this->getConnection(); $bind = [':currency_from' => $code]; $select = $connection->select()->from( - $this->getTable('directory_currency_rate'), + $this->_currencyRateTable, ['currency_to', 'rate'] )->where( 'currency_from = :currency_from' diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php index 69d500960d3f0..47f4fb0a6c7f3 100644 --- a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php @@ -13,8 +13,7 @@ use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddDataForIndia - * @package Magento\Directory\Setup\Patch\Data + * Add Regions for India. */ class AddDataForIndia implements DataPatchInterface, PatchVersionInterface { @@ -29,7 +28,7 @@ class AddDataForIndia implements DataPatchInterface, PatchVersionInterface private $dataInstallerFactory; /** - * AddDataForCroatia constructor. + * AddDataForIndia constructor. * * @param ModuleDataSetupInterface $moduleDataSetup * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory @@ -43,7 +42,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -103,7 +102,7 @@ private function getDataForIndia() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -113,7 +112,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -121,7 +120,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForMexico.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForMexico.php new file mode 100644 index 0000000000000..32bdf90800d6b --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForMexico.php @@ -0,0 +1,127 @@ +moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForMexico() + ); + } + + /** + * Mexican states data. + * + * @return array + */ + private function getDataForMexico() + { + return [ + ['MX', 'AGU', 'Aguascalientes'], + ['MX', 'BCN', 'Baja California'], + ['MX', 'BCS', 'Baja California Sur'], + ['MX', 'CAM', 'Campeche'], + ['MX', 'CHP', 'Chiapas'], + ['MX', 'CHH', 'Chihuahua'], + ['MX', 'CMX', 'Ciudad de México'], + ['MX', 'COA', 'Coahuila'], + ['MX', 'COL', 'Colima'], + ['MX', 'DUR', 'Durango'], + ['MX', 'MEX', 'Estado de México'], + ['MX', 'GUA', 'Guanajuato'], + ['MX', 'GRO', 'Guerrero'], + ['MX', 'HID', 'Hidalgo'], + ['MX', 'JAL', 'Jalisco'], + ['MX', 'MIC', 'Michoacán'], + ['MX', 'MOR', 'Morelos'], + ['MX', 'NAY', 'Nayarit'], + ['MX', 'NLE', 'Nuevo León'], + ['MX', 'OAX', 'Oaxaca'], + ['MX', 'PUE', 'Puebla'], + ['MX', 'QUE', 'Querétaro'], + ['MX', 'ROO', 'Quintana Roo'], + ['MX', 'SLP', 'San Luis Potosí'], + ['MX', 'SIN', 'Sinaloa'], + ['MX', 'SON', 'Sonora'], + ['MX', 'TAB', 'Tabasco'], + ['MX', 'TAM', 'Tamaulipas'], + ['MX', 'TLA', 'Tlaxcala'], + ['MX', 'VER', 'Veracruz'], + ['MX', 'YUC', 'Yucatán'], + ['MX', 'ZAC', 'Zacatecas'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + AddDataForAustralia::class, + AddDataForCroatia::class, + AddDataForIndia::class, + ]; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.4'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php index 9b52bae26f90f..e594be90b26dd 100644 --- a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php +++ b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php @@ -68,7 +68,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @return void @@ -91,7 +91,7 @@ public function testGetConfigCurrencies(string $areCode) ->method('getCode') ->willReturn('testCode'); - if ($areCode === Area::AREA_ADMINHTML) { + if (in_array($areCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { $this->storeManager->expects(self::once()) ->method('getStores') ->willReturn([$store]); @@ -121,6 +121,7 @@ public function getConfigCurrenciesDataProvider() { return [ ['areaCode' => Area::AREA_ADMINHTML], + ['areaCode' => Area::AREA_CRONTAB], ['areaCode' => Area::AREA_FRONTEND], ]; } diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Countries.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Countries.php new file mode 100644 index 0000000000000..dc788801f3e6a --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Countries.php @@ -0,0 +1,63 @@ +dataProcessor = $dataProcessor; + $this->countryInformationAcquirer = $countryInformationAcquirer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $countries = $this->countryInformationAcquirer->getCountriesInfo(); + + $output = []; + foreach ($countries as $country) { + $output[] = $this->dataProcessor->buildOutputDataArray($country, CountryInformationInterface::class); + } + + return $output; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country.php new file mode 100644 index 0000000000000..ea39f12a7bcb5 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country.php @@ -0,0 +1,67 @@ +dataProcessor = $dataProcessor; + $this->countryInformationAcquirer = $countryInformationAcquirer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + try { + $country = $this->countryInformationAcquirer->getCountryInfo($args['id']); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__($exception->getMessage()), $exception); + } + + return $this->dataProcessor->buildOutputDataArray( + $country, + CountryInformationInterface::class + ); + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency.php new file mode 100644 index 0000000000000..fb2db6c312ac1 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency.php @@ -0,0 +1,59 @@ +dataProcessor = $dataProcessor; + $this->currencyInformationAcquirer = $currencyInformationAcquirer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return $this->dataProcessor->buildOutputDataArray( + $this->currencyInformationAcquirer->getCurrencyInfo(), + CurrencyInformationInterface::class + ); + } +} diff --git a/app/code/Magento/DirectoryGraphQl/README.md b/app/code/Magento/DirectoryGraphQl/README.md new file mode 100644 index 0000000000000..1a5c969b39edf --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/README.md @@ -0,0 +1,4 @@ +# DirectoryGraphQl + +**DirectoryGraphQl** provides type and resolver information for the GraphQl module +to generate directory information endpoints. diff --git a/app/code/Magento/DirectoryGraphQl/Test/Mftf/README.md b/app/code/Magento/DirectoryGraphQl/Test/Mftf/README.md new file mode 100644 index 0000000000000..8e2e188c1fe97 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Directory Graph Ql Functional Tests + +The Functional Test Module for **Magento Directory Graph Ql** module. diff --git a/app/code/Magento/DirectoryGraphQl/composer.json b/app/code/Magento/DirectoryGraphQl/composer.json new file mode 100644 index 0000000000000..0a81102a92767 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-directory-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/module-directory": "*", + "magento/framework": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\DirectoryGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/DirectoryGraphQl/etc/module.xml b/app/code/Magento/DirectoryGraphQl/etc/module.xml new file mode 100644 index 0000000000000..5d6ec613f36b3 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..8da1920f9a444 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -0,0 +1,39 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + currency: Currency @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency") @doc(description: "The currency query returns information about store currency.") + countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") + country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") +} + +type Currency { + base_currency_code: String + base_currency_symbol: String + default_display_currecy_code: String @deprecated(reason: "Symbol was missed. Use `default_display_currency_code`.") + default_display_currency_code: String + default_display_currecy_symbol: String @deprecated(reason: "Symbol was missed. Use `default_display_currency_symbol`.") + default_display_currency_symbol: String + available_currency_codes: [String] + exchange_rates: [ExchangeRate] +} + +type ExchangeRate { + currency_to: String + rate: Float +} + +type Country { + id: String + two_letter_abbreviation: String + three_letter_abbreviation: String + full_name_locale: String + full_name_english: String + available_regions: [Region] +} + +type Region { + id: Int + code: String + name: String +} diff --git a/app/code/Magento/DirectoryGraphQl/registration.php b/app/code/Magento/DirectoryGraphQl/registration.php new file mode 100644 index 0000000000000..6bb7fd8d4e44d --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/registration.php @@ -0,0 +1,9 @@ + - */ namespace Magento\Downloadable\Block\Adminhtml\Catalog\Product\Composite\Fieldset; /** + * Adminhtml block for fieldset of downloadable product + * * @api * @since 100.0.2 + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends \Magento\Downloadable\Block\Catalog\Product\Links { diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php index e2694b3b93bb9..8fdf1d395308e 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php @@ -15,6 +15,8 @@ * Adminhtml catalog product downloadable items tab and form * * @author Magento Core Team + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends Widget implements TabInterface { @@ -134,6 +136,8 @@ public function isHidden() } /** + * Get group code + * * @return string */ public function getGroupCode() @@ -152,6 +156,8 @@ public function getContentTabId() } /** + * Is downloadable + * * @return bool */ public function isDownloadable() @@ -160,7 +166,7 @@ public function isDownloadable() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php index 947e6dc1e8339..47c66c98fc8fb 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php @@ -10,6 +10,9 @@ * * @author Magento Core Team * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links */ class Links extends \Magento\Backend\Block\Template { @@ -434,6 +437,8 @@ public function getConfig() } /** + * Is single store mode + * * @return bool */ public function isSingleStoreMode() @@ -442,8 +447,11 @@ public function isSingleStoreMode() } /** + * Get base currency code + * * @param null|string|bool|int|\Magento\Store\Model\Store $storeId $storeId * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getBaseCurrencyCode($storeId) { @@ -451,8 +459,11 @@ public function getBaseCurrencyCode($storeId) } /** + * Get base currency symbol + * * @param null|string|bool|int|\Magento\Store\Model\Store $storeId $storeId * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getBaseCurrencySymbol($storeId) { diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php index 3c86bfb2f8d00..f245aeeeead67 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php @@ -9,6 +9,9 @@ * Adminhtml catalog product downloadable items tab links section * * @author Magento Core Team + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples */ class Samples extends \Magento\Backend\Block\Widget { diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php index 1ef72f1deeccd..fe430566d63ce 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php @@ -6,6 +6,13 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; +/** + * Class Form + * + * @deprecated since downloadable information rendering moved to UI components. + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + */ class Form extends \Magento\Catalog\Controller\Adminhtml\Product\Edit { /** diff --git a/app/code/Magento/Downloadable/Controller/Download/Link.php b/app/code/Magento/Downloadable/Controller/Download/Link.php index 765546d080e5d..4766f1699afb6 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Link.php +++ b/app/code/Magento/Downloadable/Controller/Download/Link.php @@ -1,15 +1,21 @@ setStatus(PurchasedLink::LINK_STATUS_EXPIRED); } $linkPurchasedItem->save(); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { $this->messageManager->addError(__('Something went wrong while getting the requested content.')); diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php index 76ec791611c9c..f40df744dd3ea 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -1,21 +1,26 @@ _processDownload($resource, $resourceType); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { $this->messageManager->addError( diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php index 4a4f88d81b37a..ac9eeac678f8d 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php @@ -1,21 +1,26 @@ _processDownload($resource, $resourceType); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { $this->messageManager->addError( diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 150a5ec474f36..6b7db3af51195 100644 --- a/app/code/Magento/Downloadable/Helper/Download.php +++ b/app/code/Magento/Downloadable/Helper/Download.php @@ -13,6 +13,7 @@ /** * Downloadable Products Download Helper * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Download extends \Magento\Framework\App\Helper\AbstractHelper { @@ -186,19 +187,20 @@ public function getFileSize() public function getContentType() { $this->_getHandle(); - if ($this->_linkType == self::LINK_TYPE_FILE) { - if (function_exists( - 'mime_content_type' - ) && ($contentType = mime_content_type( - $this->_workingDirectory->getAbsolutePath($this->_resourceFile) - )) + if ($this->_linkType === self::LINK_TYPE_FILE) { + if (function_exists('mime_content_type') + && ($contentType = mime_content_type( + $this->_workingDirectory->getAbsolutePath($this->_resourceFile) + )) ) { return $contentType; - } else { - return $this->_downloadableFile->getFileType($this->_resourceFile); } - } elseif ($this->_linkType == self::LINK_TYPE_URL) { - return $this->_handle->stat($this->_resourceFile)['type']; + return $this->_downloadableFile->getFileType($this->_resourceFile); + } + if ($this->_linkType === self::LINK_TYPE_URL) { + return (is_array($this->_handle->stat($this->_resourceFile)['type']) + ? end($this->_handle->stat($this->_resourceFile)['type']) + : $this->_handle->stat($this->_resourceFile)['type']); } return $this->_contentType; } @@ -252,10 +254,21 @@ public function setResource($resourceFile, $linkType = self::LINK_TYPE_FILE) ); } } - + $this->_resourceFile = $resourceFile; + + /** + * check header for urls + */ + if ($linkType === self::LINK_TYPE_URL) { + $headers = array_change_key_case(get_headers($this->_resourceFile, 1), CASE_LOWER); + if (isset($headers['location'])) { + $this->_resourceFile = is_array($headers['location']) ? current($headers['location']) + : $headers['location']; + } + } + $this->_linkType = $linkType; - return $this; } diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Link.php b/app/code/Magento/Downloadable/Model/ResourceModel/Link.php index 24d1d7831c9e3..df8427bdde652 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Link.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Link.php @@ -5,10 +5,6 @@ */ namespace Magento\Downloadable\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\EntityManager\MetadataPool; - /** * Downloadable Product Samples resource model * @@ -17,11 +13,6 @@ */ class Link extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { - /** - * @var MetadataPool - */ - private $metadataPool; - /** * Catalog data * @@ -210,10 +201,7 @@ public function getSearchableData($productId, $storeId) [] )->join( ['cpe' => $this->getTable('catalog_product_entity')], - sprintf( - 'cpe.entity_id = m.product_id', - $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField() - ), + 'cpe.entity_id = m.product_id', [] )->joinLeft( ['st' => $this->getTable('downloadable_link_title')], @@ -228,22 +216,12 @@ public function getSearchableData($productId, $storeId) } /** + * Get Currency model. + * * @return \Magento\Directory\Model\Currency */ protected function _createCurrency() { return $this->_currencyFactory->create(); } - - /** - * Get MetadataPool instance - * @return MetadataPool - */ - private function getMetadataPool() - { - if (!$this->metadataPool) { - $this->metadataPool = ObjectManager::getInstance()->get(MetadataPool::class); - } - return $this->metadataPool; - } } diff --git a/app/code/Magento/Downloadable/Model/SampleRepository.php b/app/code/Magento/Downloadable/Model/SampleRepository.php index 0c7d2b96f1b53..07c7631fade13 100644 --- a/app/code/Magento/Downloadable/Model/SampleRepository.php +++ b/app/code/Magento/Downloadable/Model/SampleRepository.php @@ -23,6 +23,7 @@ /** * Class SampleRepository + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SampleRepository implements \Magento\Downloadable\Api\SampleRepositoryInterface @@ -100,7 +101,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -209,6 +210,8 @@ public function save( } /** + * Save sample. + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param SampleInterface $sample * @param bool $isGlobalScopeContent @@ -257,6 +260,8 @@ protected function saveSample( } /** + * Update sample. + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param SampleInterface $sample * @param bool $isGlobalScopeContent @@ -308,15 +313,18 @@ protected function updateSample( $existingSample->setTitle($sample->getTitle()); } - if ($sample->getSampleType() === 'file' && $sample->getSampleFileContent() === null) { - $sample->setSampleFile($existingSample->getSampleFile()); + if ($sample->getSampleType() === 'file' + && $sample->getSampleFileContent() === null + && $sample->getSampleFile() !== null + ) { + $existingSample->setSampleFile($sample->getSampleFile()); } $this->saveSample($product, $sample, $isGlobalScopeContent); return $existingSample->getId(); } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($id) { diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 64305cfce9b08..7c1d2748a3e9c 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -9,6 +9,8 @@ use Magento\Store\Model\ScopeInterface; /** + * Saves data from order to purchased links. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SaveDownloadableOrderItemObserver implements ObserverInterface @@ -92,9 +94,15 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($purchasedLink->getId()) { return $this; } + $storeId = $orderItem->getOrder()->getStoreId(); + $orderStatusToEnableItem = $this->_scopeConfig->getValue( + \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, + ScopeInterface::SCOPE_STORE, + $storeId + ); if (!$product) { $product = $this->_createProductModel()->setStoreId( - $orderItem->getOrder()->getStoreId() + $storeId )->load( $orderItem->getProductId() ); @@ -150,6 +158,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) )->setNumberOfDownloadsBought( $numberOfDownloads )->setStatus( + \Magento\Sales\Model\Order\Item::STATUS_PENDING == $orderStatusToEnableItem ? + \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE : \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING )->setCreatedAt( $orderItem->getCreatedAt() @@ -165,6 +175,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } /** + * Create purchased model. + * * @return \Magento\Downloadable\Model\Link\Purchased */ protected function _createPurchasedModel() @@ -173,6 +185,8 @@ protected function _createPurchasedModel() } /** + * Create product model. + * * @return \Magento\Catalog\Model\Product */ protected function _createProductModel() @@ -181,6 +195,8 @@ protected function _createProductModel() } /** + * Create purchased item model. + * * @return \Magento\Downloadable\Model\Link\Purchased\Item */ protected function _createPurchasedItemModel() @@ -189,6 +205,8 @@ protected function _createPurchasedItemModel() } /** + * Create items collection. + * * @return \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\Collection */ protected function _createItemsCollection() diff --git a/app/code/Magento/Downloadable/Observer/UpdateLinkPurchasedObserver.php b/app/code/Magento/Downloadable/Observer/UpdateLinkPurchasedObserver.php new file mode 100644 index 0000000000000..db391ccda6866 --- /dev/null +++ b/app/code/Magento/Downloadable/Observer/UpdateLinkPurchasedObserver.php @@ -0,0 +1,80 @@ +scopeConfig = $scopeConfig; + $this->purchasedFactory = $purchasedFactory; + $this->objectCopyService = $objectCopyService; + } + + /** + * Re-save order data after order update. + * + * @param \Magento\Framework\Event\Observer $observer + * @return $this + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $order = $observer->getEvent()->getOrder(); + + if (!$order->getId()) { + //order not saved in the database + return $this; + } + + $purchasedLinks = $this->purchasedFactory->create()->addFieldToFilter( + 'order_id', + ['eq' => $order->getId()] + ); + + foreach ($purchasedLinks as $linkPurchased) { + $this->objectCopyService->copyFieldsetToTarget( + \downloadable_sales_copy_order::class, + 'to_downloadable', + $order, + $linkPurchased + ); + $linkPurchased->save(); + } + + return $this; + } +} diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 0000000000000..b84dbff1154cf --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml index 38ac2c99e4756..08f1c2349357d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -31,6 +31,28 @@ magento-logo.png https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg + + link-1 + 2.43 + 2 + url + http://example.com + url + http://example.com + 0 + 1 + + + link-2 + 3 + 3 + url + http://example.com + url + http://example.com + 1 + 2 + SampleFile Upload File diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 6a91b60dcb588..c836b0d90d13d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -19,6 +19,35 @@ 1 downloadableproduct + + downloadableproduct + downloadable + 4 + DownloadableProduct + 50.99 + 100 + 0 + 1 + downloadableproduct + 4 + EavStockItem + CustomAttributeCategoryIds + downloadableLink1 + + + downloadableproduct + downloadable + 4 + DownloadableProduct + 50.99 + 100 + 0 + 1 + downloadableproduct + CustomAttributeCategoryIds + downloadableLink1 + downloadableLink2 + api-downloadable-product downloadable diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 0000000000000..39b4e303d5165 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml index 88dcca0958719..a7acdfded29b6 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml new file mode 100755 index 0000000000000..55740af4d834f --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -0,0 +1,31 @@ + + + + + + + + + + <description value="After selecting a downloadable product when adding Admin should be switch to simple implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10929"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="downloadable"/> + </actionGroup> + <!-- Fill form for Virtual Product Type --> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml new file mode 100644 index 0000000000000..d7e93d3429b96 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml @@ -0,0 +1,57 @@ +<?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="AdminDeleteDownloadableProductTest"> + <annotations> + <features value="Downloadable"/> + <title value="Delete Downloadable Product"/> + <description value="Admin should be able to delete a downloadable product"/> + <testCaseId value="MC-11018"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="DownloadableProductWithTwoLink" stepKey="createDownloadableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + <createData entity="downloadableLink2" stepKey="addDownloadableLink2"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteDownloadableProductFilteredBySkuAndName"> + <argument name="product" value="$$createDownloadableProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.name$$)}}" stepKey="amOnDownloadableProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDownloadableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createDownloadableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createDownloadableProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml index af5d20b075d12..66177b6875dd9 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchDownloadableByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="Downloadable"/> diff --git a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php index 8b9900d747ce5..06b29fce1cd14 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php @@ -5,6 +5,14 @@ */ namespace Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class LinksTest + * + * @package Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links + */ class LinksTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php index e850923bbd068..f0423606add55 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php @@ -5,6 +5,14 @@ */ namespace Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class SamplesTest + * + * @package Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples + */ class SamplesTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php index a352c4bdf7bc3..c4824f913daf8 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php @@ -3,22 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; -use Magento\Downloadable\Model\Source\TypeUpload; use Magento\Downloadable\Model\Source\Shareable; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Downloadable\Model\Source\TypeUpload; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with links + * Class adds a grid with links. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Links extends AbstractModifier @@ -86,7 +88,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -101,7 +103,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -160,6 +162,8 @@ public function modifyMeta(array $meta) } /** + * Returns configuration for dynamic rows + * * @return array */ protected function getDynamicRows() @@ -180,6 +184,8 @@ protected function getDynamicRows() } /** + * Returns Record column configuration + * * @return array */ protected function getRecord() @@ -221,6 +227,8 @@ protected function getRecord() } /** + * Returns Title column configuration + * * @return array */ protected function getTitleColumn() @@ -232,12 +240,14 @@ protected function getTitleColumn() 'label' => __('Title'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'title', + 'labelVisible' => false, 'validation' => [ 'required-entry' => true, ], @@ -247,6 +257,8 @@ protected function getTitleColumn() } /** + * Returns Price column configuration + * * @return array */ protected function getPriceColumn() @@ -258,6 +270,7 @@ protected function getPriceColumn() 'label' => __('Price'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $priceField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -265,6 +278,7 @@ protected function getPriceColumn() 'dataType' => Form\Element\DataType\Number::NAME, 'component' => 'Magento_Downloadable/js/components/price-handler', 'dataScope' => 'price', + 'labelVisible' => false, 'addbefore' => $this->locator->getStore()->getBaseCurrency() ->getCurrencySymbol(), 'validation' => [ @@ -281,6 +295,8 @@ protected function getPriceColumn() } /** + * Returns File column configuration + * * @return array */ protected function getFileColumn() @@ -292,6 +308,7 @@ protected function getFileColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 30, ]; $fileTypeField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -302,6 +319,7 @@ protected function getFileColumn() 'options' => $this->typeUpload->toOptionArray(), 'typeFile' => 'links_file', 'typeUrl' => 'link_url', + 'labelVisible' => false, ]; $fileLinkUrl['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -344,6 +362,8 @@ protected function getFileColumn() } /** + * Returns Sample column configuration + * * @return array */ protected function getSampleColumn() @@ -355,6 +375,7 @@ protected function getSampleColumn() 'label' => __('Sample'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 40, ]; $sampleTypeField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -363,6 +384,7 @@ protected function getSampleColumn() 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'sample.type', 'options' => $this->typeUpload->toOptionArray(), + 'labelVisible' => false, 'typeFile' => 'sample_file', 'typeUrl' => 'sample_url', ]; @@ -382,6 +404,7 @@ protected function getSampleColumn() 'component' => 'Magento_Downloadable/js/components/file-uploader', 'elementTmpl' => 'Magento_Downloadable/components/file-uploader', 'fileInputName' => 'link_samples', + 'labelVisible' => false, 'uploaderConfig' => [ 'url' => $this->urlBuilder->addSessionParam()->getUrl( 'adminhtml/downloadable_file/upload', @@ -403,6 +426,8 @@ protected function getSampleColumn() } /** + * Returns Sharable columns configuration + * * @return array */ protected function getShareableColumn() @@ -413,6 +438,7 @@ protected function getShareableColumn() 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Number::NAME, 'dataScope' => 'is_shareable', + 'sortOrder' => 50, 'options' => $this->shareable->toOptionArray(), ]; @@ -420,6 +446,8 @@ protected function getShareableColumn() } /** + * Returns max downloads column configuration + * * @return array */ protected function getMaxDownloadsColumn() @@ -431,12 +459,14 @@ protected function getMaxDownloadsColumn() 'label' => __('Max. Downloads'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 60, ]; $numberOfDownloadsField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Number::NAME, 'dataScope' => 'number_of_downloads', + 'labelVisible' => false, 'value' => 0, 'validation' => [ 'validate-zero-or-greater' => true, diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php index 1587163ba8121..81c5918eb24ae 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php @@ -3,21 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; use Magento\Downloadable\Model\Source\TypeUpload; -use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with samples + * Class adds a grid with samples. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Samples extends AbstractModifier @@ -77,7 +79,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -90,7 +92,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -135,6 +137,8 @@ public function modifyMeta(array $meta) } /** + * Returns configuration for dynamic rows + * * @return array */ protected function getDynamicRows() @@ -155,6 +159,8 @@ protected function getDynamicRows() } /** + * Returns Record column configuration + * * @return array */ protected function getRecord() @@ -192,6 +198,8 @@ protected function getRecord() } /** + * Returns Title column configuration + * * @return array */ protected function getTitleColumn() @@ -203,12 +211,14 @@ protected function getTitleColumn() 'showLabel' => false, 'label' => __('Title'), 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'title', + 'labelVisible' => false, 'validation' => [ 'required-entry' => true, ], @@ -218,6 +228,8 @@ protected function getTitleColumn() } /** + * Returns Sample column configuration + * * @return array */ protected function getSampleColumn() @@ -229,6 +241,7 @@ protected function getSampleColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $sampleType['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -236,6 +249,7 @@ protected function getSampleColumn() 'component' => 'Magento_Downloadable/js/components/upload-type-handler', 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'type', + 'labelVisible' => false, 'options' => $this->typeUpload->toOptionArray(), 'typeFile' => 'sample_file', 'typeUrl' => 'sample_url', @@ -246,6 +260,7 @@ protected function getSampleColumn() 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'sample_url', 'placeholder' => 'URL', + 'labelVisible' => false, 'validation' => [ 'required-entry' => true, 'validate-url' => true, diff --git a/app/code/Magento/Downloadable/etc/events.xml b/app/code/Magento/Downloadable/etc/events.xml index 5a985fc33802e..21cc50ddc9669 100644 --- a/app/code/Magento/Downloadable/etc/events.xml +++ b/app/code/Magento/Downloadable/etc/events.xml @@ -11,6 +11,7 @@ </event> <event name="sales_order_save_after"> <observer name="downloadable_observer" instance="Magento\Downloadable\Observer\SetLinkStatusObserver" /> + <observer name="downloadable_observer_assign_customer" instance="Magento\Downloadable\Observer\UpdateLinkPurchasedObserver" /> </event> <event name="sales_model_service_quote_submit_success"> <observer name="checkout_type_onepage_save_order_after" instance="Magento\Downloadable\Observer\SetHasDownloadableProductsObserver" /> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml index 19b485f0b782f..0352c98bfa56d 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="downloadable_items"/> <body> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml index 843f9b4025649..d424db980f7a4 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="downloadable_items"/> </page> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml index d1e551ff1c96d..9c88e1ba15c4b 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="product.composite.fieldset"> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml index 843f9b4025649..d424db980f7a4 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="downloadable_items"/> </page> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml index 958e922334db7..fec90e7049be2 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd"> <!--<referenceContainer name="product_form">--> <!--<block name="downloadable_items" class="Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable">--> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml index c86eb56a39008..c2338e30ecd3b 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> @@ -29,7 +30,7 @@ value="<?= /* @escapeNotVerified */ $_link->getId() ?>" <?= /* @escapeNotVerified */ $block->getLinkCheckedValue($_link) ?> price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_link->getPrice()) ?>"/> <?php endif; ?> - <label for="links_<?= /* @escapeNotVerified */ $_link->getId() ?>" class="label"> + <label for="links_<?= /* @escapeNotVerified */ $_link->getId() ?>" class="label admin__field-label"> <?= $block->escapeHtml($_link->getTitle()) ?> <?php if ($_link->getSampleFile() || $_link->getSampleUrl()): ?>  (<a href="<?= /* @escapeNotVerified */ $block->getLinkSampleUrl($_link) ?>" <?= $block->getIsOpenInNewWindow()?'onclick="this.target=\'_blank\'"':'' ?>><?= /* @escapeNotVerified */ __('sample') ?></a>) diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml index 7dc547c5e2752..a4443edb08e69 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml index 3ec6010218fb6..c86019d9cd20c 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml index 3645a184df216..947d1d0b38bef 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/Downloadable/view/frontend/layout/catalog_product_view_type_downloadable.xml b/app/code/Magento/Downloadable/view/frontend/layout/catalog_product_view_type_downloadable.xml index 45e5f0b8da72d..f851558c1a563 100644 --- a/app/code/Magento/Downloadable/view/frontend/layout/catalog_product_view_type_downloadable.xml +++ b/app/code/Magento/Downloadable/view/frontend/layout/catalog_product_view_type_downloadable.xml @@ -26,17 +26,6 @@ </block> </block> </referenceBlock> - <referenceContainer name="product.info.options.wrapper.bottom"> - <block class="Magento\Catalog\Pricing\Render" name="product.price.final.copy" before="-"> - <arguments> - <argument name="price_render" xsi:type="string">product.price.render.default</argument> - <argument name="price_type_code" xsi:type="string">final_price</argument> - <argument name="display_msrp_help_message" xsi:type="string">1</argument> - <argument name="zone" xsi:type="string">item_view</argument> - <argument name="id_suffix" xsi:type="string">copy-</argument> - </arguments> - </block> - </referenceContainer> <referenceBlock name="head.components"> <block class="Magento\Framework\View\Element\Js\Components" name="downloadable_page_head_components" template="Magento_Downloadable::js/components.phtml"/> </referenceBlock> diff --git a/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php b/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php index 59c007d910764..4bef5d3d57b0b 100644 --- a/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php +++ b/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php @@ -8,19 +8,21 @@ namespace Magento\DownloadableGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Downloadable\Model\Product\Type as Type; /** - * {@inheritdoc} + * @inheritdoc */ class DownloadableProductTypeResolver implements TypeResolverInterface { + const DOWNLOADABLE_PRODUCT = 'DownloadableProduct'; /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { - if (isset($data['type_id']) && $data['type_id'] == 'downloadable') { - return 'DownloadableProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_DOWNLOADABLE) { + return self::DOWNLOADABLE_PRODUCT; } return ''; } diff --git a/app/code/Magento/DownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php b/app/code/Magento/DownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php new file mode 100644 index 0000000000000..b981e02885665 --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Model\Resolver; + +use Magento\DownloadableGraphQl\Model\ResourceModel\GetPurchasedDownloadableProducts; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\UrlInterface; + +/** + * @inheritdoc + * + * Returns available downloadable products for customer + */ +class CustomerDownloadableProducts implements ResolverInterface +{ + /** + * @var GetPurchasedDownloadableProducts + */ + private $getPurchasedDownloadableProducts; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param GetPurchasedDownloadableProducts $getPurchasedDownloadableProducts + * @param UrlInterface $urlBuilder + */ + public function __construct( + GetPurchasedDownloadableProducts $getPurchasedDownloadableProducts, + UrlInterface $urlBuilder + ) { + $this->getPurchasedDownloadableProducts = $getPurchasedDownloadableProducts; + $this->urlBuilder = $urlBuilder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $currentUserId = $context->getUserId(); + $purchasedProducts = $this->getPurchasedDownloadableProducts->execute($currentUserId); + $productsData = []; + + /* The fields names are hardcoded since there's no existing name reference in the code */ + foreach ($purchasedProducts as $purchasedProduct) { + if ($purchasedProduct['number_of_downloads_bought']) { + $remainingDownloads = $purchasedProduct['number_of_downloads_bought'] - + $purchasedProduct['number_of_downloads_used']; + } else { + $remainingDownloads = __('Unlimited'); + } + + $productsData[] = [ + 'order_increment_id' => $purchasedProduct['order_increment_id'], + 'date' => $purchasedProduct['created_at'], + 'status' => $purchasedProduct['status'], + 'download_url' => $this->urlBuilder->getUrl( + 'downloadable/download/link', + ['id' => $purchasedProduct['link_hash'], '_secure' => true] + ), + 'remaining_downloads' => $remainingDownloads + ]; + } + + return ['items' => $productsData]; + } +} diff --git a/app/code/Magento/DownloadableGraphQl/Model/ResourceModel/GetPurchasedDownloadableProducts.php b/app/code/Magento/DownloadableGraphQl/Model/ResourceModel/GetPurchasedDownloadableProducts.php new file mode 100644 index 0000000000000..e8c29e90609f8 --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Model/ResourceModel/GetPurchasedDownloadableProducts.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Downloadable\Model\Link\Purchased\Item; + +/** + * Class GetPurchasedDownloadableProducts + * + * The model returns all purchased products for the specified customer + */ +class GetPurchasedDownloadableProducts +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Return available purchased products for customer + * + * @param int $customerId + * @return array + */ + public function execute(int $customerId): array + { + $connection = $this->resourceConnection->getConnection(); + $allowedItemsStatuses = [Item::LINK_STATUS_PENDING_PAYMENT, Item::LINK_STATUS_PAYMENT_REVIEW]; + $downloadablePurchasedTable = $connection->getTableName('downloadable_link_purchased'); + + /* The fields names are hardcoded since there's no existing name reference in the code */ + $selectQuery = $connection->select() + ->from($downloadablePurchasedTable) + ->joinLeft( + ['item' => $connection->getTableName('downloadable_link_purchased_item')], + "$downloadablePurchasedTable.purchased_id = item.purchased_id" + ) + ->where("$downloadablePurchasedTable.customer_id = ?", $customerId) + ->where('item.status NOT IN (?)', $allowedItemsStatuses); + + return $connection->fetchAll($selectQuery); + } +} diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 8e877ffe8360a..e2cacdf7608d6 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -1,6 +1,22 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type Query { + customerDownloadableProducts: CustomerDownloadableProducts @resolver(class: "Magento\\DownloadableGraphQl\\Model\\Resolver\\CustomerDownloadableProducts") @doc(description: "The query returns the contents of a customer's downloadable products") +} + +type CustomerDownloadableProducts { + items: [CustomerDownloadableProduct] @doc(description: "List of purchased downloadable items") +} + +type CustomerDownloadableProduct { + order_increment_id: String + date: String + status: String + download_url: String + remaining_downloads: String +} + type DownloadableProduct implements ProductInterface, CustomizableProductInterface @doc(description: "DownloadableProduct defines a product that the customer downloads") { downloadable_product_samples: [DownloadableProductSamples] @resolver(class: "Magento\\DownloadableGraphQl\\Model\\Resolver\\Product\\DownloadableOptions") @doc(description: "An array containing information about samples of this downloadable product.") downloadable_product_links: [DownloadableProductLinks] @resolver(class: "Magento\\DownloadableGraphQl\\Model\\Resolver\\Product\\DownloadableOptions") @doc(description: "An array containing information about the links for this downloadable product") diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php index c5a18a3de99c6..be9d2700664c7 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form main tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Main; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +/** + * Product attribute add/edit form main tab + */ abstract class AbstractMain extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -110,7 +108,6 @@ protected function _prepareForm() /** @var \Magento\Framework\Data\Form $form */ $form = $this->_formFactory->create( - ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']] ); @@ -280,10 +277,11 @@ protected function _initFormValues() } /** - * Processing block html after rendering + * Processing block html after rendering. + * * Adding js block to the end of this block * - * @param string $html + * @param string $html * @return string */ protected function _afterToHtml($html) diff --git a/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php b/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php index 3189041d7f716..3bc87ed977517 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php @@ -143,6 +143,7 @@ public function setRequestScope($scope) /** * Set scope visibility + * * Search value only in scope or search value in scope and global * * @param bool $flag @@ -296,7 +297,7 @@ protected function _applyOutputFilter($value) * Validate value by attribute input validation rule * * @param string $value - * @return string|true + * @return array|true * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -311,9 +312,13 @@ protected function _validateInputRule($value) if (!empty($validateRules['input_validation'])) { $label = $this->getAttribute()->getStoreLabel(); + $allowWhiteSpace = false; switch ($validateRules['input_validation']) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // Continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), diff --git a/app/code/Magento/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index 1b2cac32598e1..a52c88261166e 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/File.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/File.php @@ -146,7 +146,7 @@ protected function _validateByRules($value) return $this->_fileValidator->getMessages(); } - if (!empty($value['tmp_name']) && !is_uploaded_file($value['tmp_name'])) { + if (!empty($value['tmp_name']) && !file_exists($value['tmp_name'])) { return [__('"%1" is not a valid file.', $label)]; } diff --git a/app/code/Magento/Eav/Model/Attribute/Data/Text.php b/app/code/Magento/Eav/Model/Attribute/Data/Text.php index 0a49e012690f6..c5167821fdfce 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/Text.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/Text.php @@ -72,20 +72,17 @@ public function validateValue($value) return true; } - if (empty($value) && $value !== '0') { + if (empty($value) && $value !== '0' && $attribute->getDefaultValue() === null) { $label = __($attribute->getStoreLabel()); $errors[] = __('"%1" is a required value.', $label); } - $result = $this->validateLength($attribute, $value); - if (count($result) !== 0) { - $errors = array_merge($errors, $result); - } + $validateLengthResult = $this->validateLength($attribute, $value); + $errors = array_merge($errors, $validateLengthResult); + + $validateInputRuleResult = $this->validateInputRule($value); + $errors = array_merge($errors, $validateInputRuleResult); - $result = $this->_validateInputRule($value); - if ($result !== true) { - $errors = array_merge($errors, $result); - } if (count($errors) == 0) { return true; } @@ -141,7 +138,7 @@ public function outputValue($format = \Magento\Eav\Model\AttributeDataFactory::O * @param string $value * @return array errors */ - private function validateLength(\Magento\Eav\Model\Attribute $attribute, $value): array + private function validateLength(\Magento\Eav\Model\Attribute $attribute, string $value): array { $errors = []; $length = $this->_string->strlen(trim($value)); @@ -162,4 +159,16 @@ private function validateLength(\Magento\Eav\Model\Attribute $attribute, $value) return $errors; } + + /** + * Validate value by attribute input validation rule. + * + * @param string $value + * @return array + */ + private function validateInputRule(string $value): array + { + $result = $this->_validateInputRule($value); + return \is_array($result) ? $result : []; + } } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 0522ea0432176..1fd71e446e6bb 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; use Magento\Framework\App\Config\Element; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\DuplicateException; @@ -215,12 +216,21 @@ abstract class AbstractEntity extends AbstractResource implements EntityInterfac */ protected $objectRelationProcessor; + /** + * @var UniqueValidationInterface + */ + private $uniqueValidator; + /** * @param Context $context * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ - public function __construct(Context $context, $data = []) - { + public function __construct( + Context $context, + $data = [], + UniqueValidationInterface $uniqueValidator = null + ) { $this->_eavConfig = $context->getEavConfig(); $this->_resource = $context->getResource(); $this->_attrSetEntity = $context->getAttributeSetEntity(); @@ -229,6 +239,8 @@ public function __construct(Context $context, $data = []) $this->_universalFactory = $context->getUniversalFactory(); $this->transactionManager = $context->getTransactionManager(); $this->objectRelationProcessor = $context->getObjectRelationProcessor(); + $this->uniqueValidator = $uniqueValidator ?: + ObjectManager::getInstance()->get(UniqueValidationInterface::class); parent::__construct(); $properties = get_object_vars($this); foreach ($data as $key => $value) { @@ -488,6 +500,7 @@ public function addAttribute(AbstractAttribute $attribute, $object = null) /** * Get attributes by scope * + * @param string $suffix * @return array */ private function getAttributesByScope($suffix) @@ -958,12 +971,8 @@ public function checkAttributeUniqueValue(AbstractAttribute $attribute, $object) $data = $connection->fetchCol($select, $bind); - $objectId = $object->getData($entityIdField); - if ($objectId) { - if (isset($data[0])) { - return $data[0] == $objectId; - } - return true; + if ($object->getData($entityIdField)) { + return $this->uniqueValidator->validate($attribute, $object, $this, $entityIdField, $data); } return !count($data); @@ -1674,14 +1683,16 @@ public function saveAttribute(DataObject $object, $attributeCode) $connection->beginTransaction(); try { - $select = $connection->select()->from($table, 'value_id')->where($where); - $origValueId = $connection->fetchOne($select); + $select = $connection->select()->from($table, ['value_id', 'value'])->where($where); + $origRow = $connection->fetchRow($select); + $origValueId = $origRow['value_id'] ?? false; + $origValue = $origRow['value'] ?? null; if ($origValueId === false && $newValue !== null) { $this->_insertAttribute($object, $attribute, $newValue); } elseif ($origValueId !== false && $newValue !== null) { $this->_updateAttribute($object, $attribute, $origValueId, $newValue); - } elseif ($origValueId !== false && $newValue === null) { + } elseif ($origValueId !== false && $newValue === null && $origValue !== null) { $connection->delete($table, $where); } $this->_processAttributeValues(); @@ -1972,7 +1983,8 @@ public function afterDelete(DataObject $object) /** * Load attributes for object - * if the object will not pass all attributes for this entity type will be loaded + * + * If the object will not pass all attributes for this entity type will be loaded * * @param array $attributes * @param AbstractEntity|null $object diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index c605f3ce17e30..23054ad613c21 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -3,10 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Eav\Model\Entity; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; @@ -81,6 +82,11 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im */ protected $dateTimeFormatter; + /** + * @var AttributeCodeValidator|null + */ + private $attributeCodeValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -101,6 +107,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param AttributeCodeValidator|null $attributeCodeValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ @@ -123,7 +130,8 @@ public function __construct( DateTimeFormatterInterface $dateTimeFormatter, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + AttributeCodeValidator $attributeCodeValidator = null ) { parent::__construct( $context, @@ -146,6 +154,9 @@ public function __construct( $this->_localeResolver = $localeResolver; $this->reservedAttributeList = $reservedAttributeList; $this->dateTimeFormatter = $dateTimeFormatter; + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( + AttributeCodeValidator::class + ); } /** @@ -231,6 +242,13 @@ public function loadEntityAttributeIdBySet() */ public function beforeSave() { + if (isset($this->_data['attribute_code']) + && !$this->attributeCodeValidator->isValid($this->_data['attribute_code']) + ) { + $errorMessages = implode("\n", $this->attributeCodeValidator->getMessages()); + throw new LocalizedException(__($errorMessages)); + } + // prevent overriding product data if (isset($this->_data['attribute_code']) && $this->reservedAttributeList->isReservedAttribute($this)) { throw new LocalizedException( @@ -241,25 +259,6 @@ public function beforeSave() ); } - /** - * Check for maximum attribute_code length - */ - if (isset( - $this->_data['attribute_code'] - ) && !\Zend_Validate::is( - $this->_data['attribute_code'], - 'StringLength', - ['max' => self::ATTRIBUTE_CODE_MAX_LENGTH] - ) - ) { - throw new LocalizedException( - __( - 'The attribute code needs to be %1 characters or fewer. Re-enter the code and try again.', - self::ATTRIBUTE_CODE_MAX_LENGTH - ) - ); - } - $defaultValue = $this->getDefaultValue(); $hasDefaultValue = (string)$defaultValue != ''; @@ -295,6 +294,12 @@ public function beforeSave() } } + if ($this->getFrontendInput() == 'media_image') { + if (!$this->getFrontendModel()) { + $this->setFrontendModel(\Magento\Catalog\Model\Product\Attribute\Frontend\Image::class); + } + } + if ($this->getBackendType() == 'gallery') { if (!$this->getBackendModel()) { $this->setBackendModel(\Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend::class); @@ -316,7 +321,7 @@ public function afterSave() } /** - * @return $this + * @inheritdoc * @since 100.0.7 */ public function afterDelete() @@ -508,7 +513,7 @@ public function __sleep() public function __wakeup() { parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->_localeDate = $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $this->_localeResolver = $objectManager->get(\Magento\Framework\Locale\ResolverInterface::class); $this->reservedAttributeList = $objectManager->get(\Magento\Catalog\Model\Product\ReservedAttributeList::class); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php index 3d4c9e89a035f..2c7ea1ab9268e 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php @@ -20,6 +20,8 @@ use Magento\Eav\Model\Entity\Attribute\Source\BooleanFactory; /** + * EAV entity attribute form renderer. + * * @api * @since 100.0.2 */ @@ -234,6 +236,9 @@ protected function _getInputValidateClass() case 'alphanumeric': $class = 'validate-alphanum'; break; + case 'alphanum-with-spaces': + $class = 'validate-alphanum-with-spaces'; + break; case 'numeric': $class = 'validate-digits'; break; diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php index 0b6ac2b998de7..2e55964560588 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeGroupExtensionInterface; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Exception\LocalizedException; /** + * Entity attribute group model + * * @api * @method int getSortOrder() * @method \Magento\Eav\Model\Entity\Attribute\Group setSortOrder(int $value) @@ -27,6 +32,11 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ private $translitFilter; + /** + * @var array + */ + private $reservedSystemNames = []; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -35,7 +45,8 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Filter\Translit $translitFilter * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data + * @param array $data (optional) + * @param array $reservedSystemNames (optional) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -45,7 +56,8 @@ public function __construct( \Magento\Framework\Filter\Translit $translitFilter, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + array $reservedSystemNames = [] ) { parent::__construct( $context, @@ -56,6 +68,7 @@ public function __construct( $resourceCollection, $data ); + $this->reservedSystemNames = $reservedSystemNames; $this->translitFilter = $translitFilter; } @@ -74,6 +87,7 @@ protected function _construct() * Checks if current attribute group exists * * @return bool + * @throws LocalizedException * @codeCoverageIgnore */ public function itemExists() @@ -85,6 +99,7 @@ public function itemExists() * Delete groups * * @return $this + * @throws LocalizedException * @codeCoverageIgnore */ public function deleteGroups() @@ -110,9 +125,10 @@ public function beforeSave() ), '-' ); - if (empty($attributeGroupCode)) { + $isReservedSystemName = in_array(strtolower($attributeGroupCode), $this->reservedSystemNames); + if (empty($attributeGroupCode) || $isReservedSystemName) { // in the following code md5 is not used for security purposes - $attributeGroupCode = md5($groupName); + $attributeGroupCode = md5(strtolower($groupName)); } $this->setAttributeGroupCode($attributeGroupCode); } @@ -121,7 +137,8 @@ public function beforeSave() } /** - * {@inheritdoc} + * @inheritdoc + * * @codeCoverageIgnoreStart */ public function getAttributeGroupId() @@ -130,7 +147,7 @@ public function getAttributeGroupId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeGroupName() { @@ -138,7 +155,7 @@ public function getAttributeGroupName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeSetId() { @@ -146,7 +163,7 @@ public function getAttributeSetId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupId($attributeGroupId) { @@ -154,7 +171,7 @@ public function setAttributeGroupId($attributeGroupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupName($attributeGroupName) { @@ -162,7 +179,7 @@ public function setAttributeGroupName($attributeGroupName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeSetId($attributeSetId) { @@ -170,9 +187,9 @@ public function setAttributeSetId($attributeSetId) } /** - * {@inheritdoc} + * @inheritdoc * - * @return \Magento\Eav\Api\Data\AttributeGroupExtensionInterface|null + * @return AttributeGroupExtensionInterface|null */ public function getExtensionAttributes() { @@ -180,14 +197,13 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * - * @param \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes + * @param AttributeGroupExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes( - \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes - ) { + public function setExtensionAttributes(AttributeGroupExtensionInterface $extensionAttributes) + { return $this->_setExtensionAttributes($extensionAttributes); } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php index 0991b3f9f4b23..dd4cd4217a127 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity\Attribute\Source; /** * Entity/Attribute/Model - attribute selection source abstract - * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.NumberOfChildren) @@ -65,7 +66,7 @@ public function getOptionText($value) { $options = $this->getAllOptions(); // Fixed for tax_class_id and custom_design - if (sizeof($options) > 0) { + if (count($options) > 0) { foreach ($options as $option) { if (isset($option['value']) && $option['value'] == $value) { return isset($option['label']) ? $option['label'] : $option['value']; @@ -73,20 +74,22 @@ public function getOptionText($value) } } // End - if (isset($options[$value])) { + if (is_scalar($value) && isset($options[$value])) { return $options[$value]; } return false; } /** + * Get option id. + * * @param string $value * @return null|string */ public function getOptionId($value) { foreach ($this->getAllOptions() as $option) { - if (strcasecmp($option['label'], $value) == 0 || $option['value'] == $value) { + if ($this->mbStrcasecmp($option['label'], $value) == 0 || $option['value'] == $value) { return $option['value']; } } @@ -164,4 +167,20 @@ public function toOptionArray() { return $this->getAllOptions(); } + + /** + * Multibyte support strcasecmp function version. + * + * @param string $str1 + * @param string $str2 + * @return int + */ + private function mbStrcasecmp($str1, $str2) + { + $encoding = mb_internal_encoding(); + return strcmp( + mb_strtoupper($str1, $encoding), + mb_strtoupper($str2, $encoding) + ); + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php new file mode 100644 index 0000000000000..b68e79d7b7d20 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Eav\Model\Entity\Attribute; + +use Magento\Framework\DataObject; +use Magento\Eav\Model\Entity\AbstractEntity; + +/** + * Interface for unique attribute validator + */ +interface UniqueValidationInterface +{ + /** + * Validate if attribute value is unique + * + * @param AbstractAttribute $attribute + * @param DataObject $object + * @param AbstractEntity $entity + * @param string $entityLinkField + * @param array $entityIds + * @return bool + */ + public function validate( + AbstractAttribute $attribute, + DataObject $object, + AbstractEntity $entity, + $entityLinkField, + array $entityIds + ); +} diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php new file mode 100644 index 0000000000000..b1888b42bef92 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Eav\Model\Entity\Attribute; + +use Magento\Framework\DataObject; +use Magento\Eav\Model\Entity\AbstractEntity; + +/** + * Class for validate unique attribute value + */ +class UniqueValidator implements UniqueValidationInterface +{ + /** + * @inheritdoc + */ + public function validate( + AbstractAttribute $attribute, + DataObject $object, + AbstractEntity $entity, + $entityLinkField, + array $entityIds + ) { + if (isset($entityIds[0])) { + return $entityIds[0] == $object->getData($entityLinkField); + } + return true; + } +} diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 0eb87374f3ba3..e50abbc11e54a 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -14,6 +14,7 @@ /** * Entity/Attribute/Model - collection abstract * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -125,6 +126,7 @@ abstract class AbstractCollection extends AbstractDb implements SourceProviderIn protected $_resourceHelper; /** + * * @var \Magento\Framework\Validator\UniversalFactory */ protected $_universalFactory; @@ -172,6 +174,7 @@ public function __construct( * Initialize collection * * @return void + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ protected function _construct() { @@ -248,7 +251,7 @@ public function setEntity($entity) $this->_entity = $this->_eavEntityFactory->create()->setType($entity); } else { throw new LocalizedException( - __('The "%1" entity supplied is invalid. Verify the entity and try again.', print_r($entity, 1)) + __('The entity supplied to collection is invalid. Verify the entity and try again.') ); } return $this; @@ -282,7 +285,7 @@ public function getResource() /** * Set template object for the collection * - * @param \Magento\Framework\DataObject $object + * @param \Magento\Framework\DataObject $object * @return $this */ public function setObject($object = null) @@ -379,7 +382,7 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = if (!empty($conditionSql)) { $this->getSelect()->where($conditionSql, null, \Magento\Framework\DB\Select::TYPE_CONDITION); - $this->invalidateSize(); + $this->_totalRecords = null; } else { throw new \Magento\Framework\Exception\LocalizedException( __('Invalid attribute identifier for filter (%1)', get_class($attribute)) @@ -399,7 +402,7 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = */ public function addFieldToFilter($attribute, $condition = null) { - return $this->addAttributeToFilter($attribute, $condition); + return $this->addAttributeToFilter($attribute, $condition, 'left'); } /** @@ -1045,6 +1048,7 @@ public function importFromArray($arr) $this->_items[$entityId]->addData($row); } } + $this->_setIsLoaded(); return $this; } @@ -1148,7 +1152,6 @@ public function _loadEntities($printQuery = false, $logQuery = false) * @param bool $printQuery * @param bool $logQuery * @return $this - * @throws LocalizedException * @throws \Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1355,8 +1358,8 @@ protected function _getAttributeFieldName($attributeCode) /** * Add attribute value table to the join if it wasn't added previously * - * @param string $attributeCode - * @param string $joinType inner|left + * @param string $attributeCode + * @param string $joinType inner|left * @return $this * @throws LocalizedException * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1450,12 +1453,12 @@ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $ent /** * Adding join statement to collection select instance * - * @param string $method - * @param object $attribute - * @param string $tableAlias - * @param array $condition - * @param string $fieldCode - * @param string $fieldAlias + * @param string $method + * @param object $attribute + * @param string $tableAlias + * @param array $condition + * @param string $fieldCode + * @param string $fieldAlias * @return $this */ protected function _joinAttributeToSelect($method, $attribute, $tableAlias, $condition, $fieldCode, $fieldAlias) @@ -1703,16 +1706,4 @@ public function removeAllFieldsFromSelect() { return $this->removeAttributeToSelect(); } - - /** - * Invalidates "Total Records Count". - * Invalidates saved "Total Records Count" attribute with last counting, - * so a next calling of method getSize() will query new total records count. - * - * @return void - */ - private function invalidateSize(): void - { - $this->_totalRecords = null; - } } diff --git a/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php index e626ed35eb1e9..631bfa3c2d2b5 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php @@ -7,8 +7,11 @@ /** * Class Abstract Collection + * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCollection { @@ -18,6 +21,7 @@ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\A protected $entitySnapshot; /** + * AbstractCollection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -27,8 +31,9 @@ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\A * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - * @param mixed $connection + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ diff --git a/app/code/Magento/Eav/Model/Entity/Type.php b/app/code/Magento/Eav/Model/Entity/Type.php index 80fcfd4ab585c..444d58bf546d4 100644 --- a/app/code/Magento/Eav/Model/Entity/Type.php +++ b/app/code/Magento/Eav/Model/Entity/Type.php @@ -167,12 +167,8 @@ public function getAttributeCollection($setId = null) */ protected function _getAttributeCollection() { - $collection = $this->_attributeFactory->create()->getCollection(); - $objectsModel = $this->getAttributeModel(); - if ($objectsModel) { - $collection->setModel($objectsModel); - } - + $collection = $this->_universalFactory->create($this->getEntityAttributeCollection()); + $collection->setItemObjectClass($this->getAttributeModel()); return $collection; } diff --git a/app/code/Magento/Eav/Model/Form.php b/app/code/Magento/Eav/Model/Form.php index c8c50521f5509..a34b53eede354 100644 --- a/app/code/Magento/Eav/Model/Form.php +++ b/app/code/Magento/Eav/Model/Form.php @@ -286,7 +286,8 @@ public function getFormCode() } /** - * Return entity type instance + * Return entity type instance. + * * Return EAV entity type if entity type is not defined * * @return \Magento\Eav\Model\Entity\Type @@ -323,6 +324,8 @@ public function getAttributes() if ($this->_attributes === null) { $this->_attributes = []; $this->_userAttributes = []; + $this->_systemAttributes = []; + $this->_allowedAttributes = []; /** @var $attribute \Magento\Eav\Model\Attribute */ foreach ($this->_getFilteredFormAttributeCollection() as $attribute) { $this->_attributes[$attribute->getAttributeCode()] = $attribute; diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php index cec415e513677..6fce6bd2dc44e 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; use Magento\Eav\Model\Entity\Type; @@ -128,7 +129,7 @@ public function setEntityTypeFilter($type) /** * Specify attribute set filter * - * @param int $setId + * @param int|int[] $setId * @return $this */ public function setAttributeSetFilter($setId) @@ -183,6 +184,7 @@ public function setAttributeSetFilterBySetName($attributeSetName, $entityTypeCod /** * Specify multiple attribute sets filter + * * Result will be ordered by sort_order * * @param array $setIds @@ -225,7 +227,6 @@ public function setInAllAttributeSetsFilter(array $setIds) ->having(new \Zend_Db_Expr('COUNT(*)') . ' = ' . count($setIds)); } - //$this->getSelect()->distinct(true); $this->setOrder('is_user_defined', self::SORT_ORDER_ASC); return $this; @@ -475,7 +476,7 @@ public function addStoreLabel($storeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelectCountSql() { diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index cd2fe7477ca60..7f6dfa2a5e9ab 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -5,13 +5,19 @@ */ namespace Magento\Eav\Model\ResourceModel; +use Magento\Eav\Model\Config; use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; use Magento\Framework\Model\Entity\ScopeInterface; use Magento\Framework\Model\Entity\ScopeResolver; use Psr\Log\LoggerInterface; +/** + * EAV read handler + */ class ReadHandler implements AttributeInterface { /** @@ -30,23 +36,21 @@ class ReadHandler implements AttributeInterface private $logger; /** - * @var \Magento\Eav\Model\Config + * @var Config */ private $config; /** - * ReadHandler constructor. - * * @param MetadataPool $metadataPool * @param ScopeResolver $scopeResolver * @param LoggerInterface $logger - * @param \Magento\Eav\Model\Config $config + * @param Config $config */ public function __construct( MetadataPool $metadataPool, ScopeResolver $scopeResolver, LoggerInterface $logger, - \Magento\Eav\Model\Config $config + Config $config ) { $this->metadataPool = $metadataPool; $this->scopeResolver = $scopeResolver; @@ -86,6 +90,8 @@ private function getEntityAttributes(string $entityType, DataObject $entity): ar } /** + * Get context variables + * * @param ScopeInterface $scope * @return array */ @@ -99,6 +105,8 @@ protected function getContextVariables(ScopeInterface $scope) } /** + * Execute read handler + * * @param string $entityType * @param array $entityData * @param array $arguments @@ -129,33 +137,40 @@ public function execute($entityType, $entityData, $arguments = []) } } if (count($attributeTables)) { - $attributeTables = array_keys($attributeTables); - foreach ($attributeTables as $attributeTable) { + $identifiers = null; + foreach ($attributeTables as $attributeTable => $attributeIds) { $select = $connection->select() ->from( ['t' => $attributeTable], ['value' => 't.value', 'attribute_id' => 't.attribute_id'] ) - ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]); + ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) + ->where('attribute_id IN (?)', $attributeIds); + $attributeIdentifiers = []; foreach ($context as $scope) { //TODO: if (in table exists context field) $select->where( - $metadata->getEntityConnection()->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', + $connection->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', $this->getContextVariables($scope) - )->order('t.' . $scope->getIdentifier() . ' DESC'); + ); + $attributeIdentifiers[] = $scope->getIdentifier(); } + $attributeIdentifiers = array_unique($attributeIdentifiers); + $identifiers = array_intersect($identifiers ?? $attributeIdentifiers, $attributeIdentifiers); $selects[] = $select; } - $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression( - $selects, - \Magento\Framework\DB\Select::SQL_UNION_ALL - ); - foreach ($connection->fetchAll($unionSelect) as $attributeValue) { + $this->applyIdentifierForSelects($selects, $identifiers); + $unionSelect = new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )'); + $orderedUnionSelect = $connection->select(); + $orderedUnionSelect->from(['u' => $unionSelect]); + $this->applyIdentifierForUnion($orderedUnionSelect, $identifiers); + $attributes = $connection->fetchAll($orderedUnionSelect); + foreach ($attributes as $attributeValue) { if (isset($attributesMap[$attributeValue['attribute_id']])) { $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value']; } else { $this->logger->warning( - "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' + "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' for entity type '$entityType'." ); } @@ -163,4 +178,32 @@ public function execute($entityType, $entityData, $arguments = []) } return $entityData; } + + /** + * Apply identifiers column on select array + * + * @param Select[] $selects + * @param array $identifiers + */ + private function applyIdentifierForSelects(array $selects, array $identifiers) + { + foreach ($selects as $select) { + foreach ($identifiers as $identifier) { + $select->columns($identifier, 't'); + } + } + } + + /** + * Apply identifiers order on union select + * + * @param Select $unionSelect + * @param array $identifiers + */ + private function applyIdentifierForUnion(Select $unionSelect, array $identifiers) + { + foreach ($identifiers as $identifier) { + $unionSelect->order($identifier); + } + } } diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Code.php b/app/code/Magento/Eav/Model/Validator/Attribute/Code.php new file mode 100644 index 0000000000000..f3ee37721b8ce --- /dev/null +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Code.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\Validator\Attribute; + +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Validator\AbstractValidator; +use Zend_Validate; + +/** + * Class Code + * + * Validation EAV attribute code + */ +class Code extends AbstractValidator +{ + /** + * Validation pattern for attribute code + */ + const VALIDATION_RULE_PATTERN = '/^[a-zA-Z]+[a-zA-Z0-9_]*$/u'; + + /** + * Validates the correctness of the attribute code + * + * @param string $attributeCode + * @return bool + * @throws \Zend_Validate_Exception + */ + public function isValid($attributeCode): bool + { + $errorMessages = []; + /** + * Check attribute_code for allowed characters + */ + if (trim($attributeCode) + && !preg_match(self::VALIDATION_RULE_PATTERN, trim($attributeCode)) + ) { + $errorMessages[] = __( + 'Attribute code "%1" is invalid. Please use only letters (a-z or A-Z), ' . + 'numbers (0-9) or underscore (_) in this field, and the first character should be a letter.', + $attributeCode + ); + } + + /** + * Check attribute_code for allowed length + */ + $minLength = Attribute::ATTRIBUTE_CODE_MIN_LENGTH; + $maxLength = Attribute::ATTRIBUTE_CODE_MAX_LENGTH; + $isAllowedLength = Zend_Validate::is( + trim($attributeCode), + 'StringLength', + ['min' => $minLength, 'max' => $maxLength] + ); + if (!$isAllowedLength) { + $errorMessages[] = __( + 'An attribute code must not be less than %1 and more than %2 characters.', + $minLength, + $maxLength + ); + } + + $this->_addMessages($errorMessages); + + return !$this->hasMessages(); + } +} diff --git a/app/code/Magento/Eav/Setup/EavSetup.php b/app/code/Magento/Eav/Setup/EavSetup.php index 6e81ddc36e9c9..de285e81b1d03 100644 --- a/app/code/Magento/Eav/Setup/EavSetup.php +++ b/app/code/Magento/Eav/Setup/EavSetup.php @@ -9,13 +9,15 @@ use Magento\Eav\Model\Entity\Setup\Context; use Magento\Eav\Model\Entity\Setup\PropertyMapperInterface; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; +use Magento\Eav\Model\Validator\Attribute\Code; use Magento\Framework\App\CacheInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Setup\ModuleDataSetupInterface; /** + * Base eav setup class. + * * @api * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -80,6 +82,11 @@ class EavSetup */ private $_defaultAttributeSetName = 'Default'; + /** + * @var Code + */ + private $attributeCodeValidator; + /** * Init * @@ -87,21 +94,27 @@ class EavSetup * @param Context $context * @param CacheInterface $cache * @param CollectionFactory $attrGroupCollectionFactory + * @param Code|null $attributeCodeValidator */ public function __construct( ModuleDataSetupInterface $setup, Context $context, CacheInterface $cache, - CollectionFactory $attrGroupCollectionFactory + CollectionFactory $attrGroupCollectionFactory, + Code $attributeCodeValidator = null ) { $this->cache = $cache; $this->attrGroupCollectionFactory = $attrGroupCollectionFactory; $this->attributeMapper = $context->getAttributeMapper(); $this->setup = $setup; + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( + Code::class + ); } /** - * Gets setup model + * Gets setup model. + * * @deprecated * @return ModuleDataSetupInterface */ @@ -568,6 +581,8 @@ public function addAttributeGroup($entityTypeId, $setId, $name, $sortOrder = nul } /** + * Convert group name to attribute group code. + * * @param string $groupName * @return string * @since 100.1.0 @@ -774,38 +789,6 @@ private function _getValue($array, $key, $default = null) return isset($array[$key]) ? $array[$key] : $default; } - /** - * Validate attribute data before insert into table - * - * @param array $data - * @return true - * @throws LocalizedException - */ - private function _validateAttributeData($data) - { - $minLength = \Magento\Eav\Model\Entity\Attribute::ATTRIBUTE_CODE_MIN_LENGTH; - $maxLength = \Magento\Eav\Model\Entity\Attribute::ATTRIBUTE_CODE_MAX_LENGTH; - $attributeCode = isset($data['attribute_code']) ? $data['attribute_code'] : ''; - - $isAllowedLength = \Zend_Validate::is( - trim($attributeCode), - 'StringLength', - ['min' => $minLength, 'max' => $maxLength] - ); - - if (!$isAllowedLength) { - $errorMessage = __( - 'An attribute code must not be less than %1 and more than %2 characters.', - $minLength, - $maxLength - ); - - throw new LocalizedException($errorMessage); - } - - return true; - } - /** * Add attribute to an entity type * @@ -815,6 +798,8 @@ private function _validateAttributeData($data) * @param string $code * @param array $attr * @return $this + * @throws LocalizedException + * @throws \Zend_Validate_Exception */ public function addAttribute($entityTypeId, $code, array $attr) { @@ -825,7 +810,7 @@ public function addAttribute($entityTypeId, $code, array $attr) $this->attributeMapper->map($attr, $entityTypeId) ); - $this->_validateAttributeData($data); + $this->validateAttributeCode($data); $sortOrder = isset($attr['sort_order']) ? $attr['sort_order'] : null; $attributeId = $this->getAttribute($entityTypeId, $code, 'attribute_id'); @@ -1063,7 +1048,7 @@ private function _updateAttributeAdditionalData($entityTypeId, $id, $field, $val return $this; } } - + $attributeId = $this->getAttributeId($entityTypeId, $id); if (false === $attributeId) { throw new LocalizedException(__('Attribute with ID: "%1" does not exist', $id)); @@ -1546,4 +1531,21 @@ private function _insertAttributeAdditionalData($entityTypeId, array $data) return $this; } + + /** + * Validate attribute code. + * + * @param array $data + * @throws LocalizedException + * @throws \Zend_Validate_Exception + */ + private function validateAttributeCode(array $data): void + { + $attributeCode = $data['attribute_code'] ?? ''; + if (!$this->attributeCodeValidator->isValid($attributeCode)) { + $errorMessage = implode('\n', $this->attributeCodeValidator->getMessages()); + + throw new LocalizedException(__($errorMessage)); + } + } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php index bbbe712b2bb42..331d1e6216ae5 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php @@ -6,12 +6,15 @@ namespace Magento\Eav\Test\Unit\Model\Attribute\Data; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\StringUtils; + class TextTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Eav\Model\Attribute\Data\Text */ - protected $_model; + private $model; /** * {@inheritDoc} @@ -21,10 +24,10 @@ protected function setUp() $locale = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $helper = $this->createMock(\Magento\Framework\Stdlib\StringUtils::class); + $helper = new StringUtils; - $this->_model = new \Magento\Eav\Model\Attribute\Data\Text($locale, $logger, $localeResolver, $helper); - $this->_model->setAttribute( + $this->model = new \Magento\Eav\Model\Attribute\Data\Text($locale, $logger, $localeResolver, $helper); + $this->model->setAttribute( $this->createAttribute( [ 'store_label' => 'Test', @@ -41,7 +44,7 @@ protected function setUp() */ protected function tearDown() { - $this->_model = null; + $this->model = null; } /** @@ -51,7 +54,7 @@ public function testValidateValueString(): void { $inputValue = '0'; $expectedResult = true; - $this->assertEquals($expectedResult, $this->_model->validateValue($inputValue)); + self::assertEquals($expectedResult, $this->model->validateValue($inputValue)); } /** @@ -61,8 +64,8 @@ public function testValidateValueInteger(): void { $inputValue = 0; $expectedResult = ['"Test" is a required value.']; - $result = $this->_model->validateValue($inputValue); - $this->assertEquals($expectedResult, [(string)$result[0]]); + $result = $this->model->validateValue($inputValue); + self::assertEquals($expectedResult, [(string)$result[0]]); } /** @@ -79,12 +82,106 @@ public function testWithoutLengthValidation(): void ]; $defaultAttributeData['validate_rules']['min_text_length'] = 2; - $this->_model->setAttribute($this->createAttribute($defaultAttributeData)); - $this->assertEquals($expectedResult, $this->_model->validateValue('t')); + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue('t')); $defaultAttributeData['validate_rules']['max_text_length'] = 3; - $this->_model->setAttribute($this->createAttribute($defaultAttributeData)); - $this->assertEquals($expectedResult, $this->_model->validateValue('test')); + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue('test')); + } + + /** + * Test of alphanumeric validation. + * + * @param {String} $value - provided value + * @param {Boolean|Array} $expectedResult - validation result + * @return void + * @throws LocalizedException + * @dataProvider alphanumDataProvider + */ + public function testAlphanumericValidation($value, $expectedResult): void + { + $defaultAttributeData = [ + 'store_label' => 'Test', + 'attribute_code' => 'test', + 'is_required' => 1, + 'validate_rules' => [ + 'min_text_length' => 0, + 'max_text_length' => 10, + 'input_validation' => 'alphanumeric' + ], + ]; + + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue($value)); + } + + /** + * Provides possible input values. + * + * @return array + */ + public function alphanumDataProvider(): array + { + return [ + ['QazWsx', true], + ['QazWsx123', true], + ['QazWsx 123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'] + ], + ['QazWsx_123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'] + ], + ['QazWsx12345', [ + __('"%1" length must be equal or less than %2 characters.', 'Test', 10)] + ], + ]; + } + + /** + * Test of alphanumeric validation with spaces. + * + * @param {String} $value - provided value + * @param {Boolean|Array} $expectedResult - validation result + * @return void + * @throws LocalizedException + * @dataProvider alphanumWithSpacesDataProvider + */ + public function testAlphanumericValidationWithSpaces($value, $expectedResult): void + { + $defaultAttributeData = [ + 'store_label' => 'Test', + 'attribute_code' => 'test', + 'is_required' => 1, + 'validate_rules' => [ + 'min_text_length' => 0, + 'max_text_length' => 10, + 'input_validation' => 'alphanum-with-spaces' + ], + ]; + + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue($value)); + } + + /** + * Provides possible input values. + * + * @return array + */ + public function alphanumWithSpacesDataProvider(): array + { + return [ + ['QazWsx', true], + ['QazWsx123', true], + ['QazWsx 123', true], + ['QazWsx_123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'] + ], + ['QazWsx12345', [ + __('"%1" length must be equal or less than %2 characters.', 'Test', 10)] + ], + ]; } /** diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php index a61c9ef447458..fd4f7472b2fa4 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Eav\Test\Unit\Model\Entity\Attribute\Frontend; use Magento\Eav\Model\Entity\Attribute\Frontend\DefaultFrontend; @@ -13,43 +15,44 @@ use Magento\Framework\App\CacheInterface; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use PHPUnit\Framework\MockObject\MockObject; class DefaultFrontendTest extends \PHPUnit\Framework\TestCase { /** * @var DefaultFrontend */ - protected $model; + private $model; /** - * @var BooleanFactory|\PHPUnit_Framework_MockObject_MockObject + * @var BooleanFactory | MockObject */ - protected $booleanFactory; + private $booleanFactory; /** - * @var Serializer|\PHPUnit_Framework_MockObject_MockObject + * @var Serializer| MockObject */ - private $serializerMock; + private $serializer; /** - * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface | MockObject */ - private $storeManagerMock; + private $storeManager; /** - * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreInterface | MockObject */ - private $storeMock; + private $store; /** - * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CacheInterface | MockObject */ - private $cacheMock; + private $cache; /** - * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractAttribute | MockObject */ - private $attributeMock; + private $attribute; /** * @var array @@ -57,10 +60,13 @@ class DefaultFrontendTest extends \PHPUnit\Framework\TestCase private $cacheTags; /** - * @var AbstractSource|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractSource | MockObject */ - private $sourceMock; + private $source; + /** + * @inheritdoc + */ protected function setUp() { $this->cacheTags = ['tag1', 'tag2']; @@ -68,111 +74,108 @@ protected function setUp() $this->booleanFactory = $this->getMockBuilder(BooleanFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->serializerMock = $this->getMockBuilder(Serializer::class) + $this->serializer = $this->getMockBuilder(Serializer::class) ->getMock(); - $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) ->getMockForAbstractClass(); - $this->storeMock = $this->getMockBuilder(StoreInterface::class) + $this->store = $this->getMockBuilder(StoreInterface::class) ->getMockForAbstractClass(); - $this->cacheMock = $this->getMockBuilder(CacheInterface::class) + $this->cache = $this->getMockBuilder(CacheInterface::class) ->getMockForAbstractClass(); - $this->attributeMock = $this->getMockBuilder(AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getSource']) - ->getMockForAbstractClass(); - $this->sourceMock = $this->getMockBuilder(AbstractSource::class) + $this->attribute = $this->createAttribute(); + $this->source = $this->getMockBuilder(AbstractSource::class) ->disableOriginalConstructor() ->setMethods(['getAllOptions']) ->getMockForAbstractClass(); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->model = $objectManager->getObject( - DefaultFrontend::class, - [ - '_attrBooleanFactory' => $this->booleanFactory, - 'cache' => $this->cacheMock, - 'storeManager' => $this->storeManagerMock, - 'serializer' => $this->serializerMock, - '_attribute' => $this->attributeMock, - 'cacheTags' => $this->cacheTags - ] + $this->model = new DefaultFrontend( + $this->booleanFactory, + $this->cache, + null, + $this->cacheTags, + $this->storeManager, + $this->serializer ); + + $this->model->setAttribute($this->attribute); } public function testGetClassEmpty() { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute | MockObject $attribute */ + $attribute = $this->createAttribute(); + $attribute->method('getIsRequired') ->willReturn(false); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attribute->method('getFrontendClass') ->willReturn(''); - $attributeMock->expects($this->exactly(2)) + $attribute->expects($this->exactly(2)) ->method('getValidateRules') ->willReturn(''); - $this->model->setAttribute($attributeMock); - $this->assertEmpty($this->model->getClass()); + $this->model->setAttribute($attribute); + + self::assertEmpty($this->model->getClass()); } - public function testGetClass() + /** + * Validates generated html classes. + * + * @param String $validationRule + * @param String $expectedClass + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testGetClass(String $validationRule, String $expectedClass): void { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute | MockObject $attribute */ + $attribute = $this->createAttribute(); + $attribute->method('getIsRequired') ->willReturn(true); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attribute->method('getFrontendClass') ->willReturn(''); - $attributeMock->expects($this->exactly(3)) + $attribute->expects($this->exactly(3)) ->method('getValidateRules') ->willReturn([ - 'input_validation' => 'alphanumeric', + 'input_validation' => $validationRule, 'min_text_length' => 1, 'max_text_length' => 2, ]); - $this->model->setAttribute($attributeMock); + $this->model->setAttribute($attribute); $result = $this->model->getClass(); - $this->assertContains('validate-alphanum', $result); - $this->assertContains('minimum-length-1', $result); - $this->assertContains('maximum-length-2', $result); - $this->assertContains('validate-length', $result); + self::assertContains($expectedClass, $result); + self::assertContains('minimum-length-1', $result); + self::assertContains('maximum-length-2', $result); + self::assertContains('validate-length', $result); + } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['alpha', 'validate-alpha'], + ['numeric', 'validate-digits'], + ['url', 'validate-url'], + ['email', 'validate-email'], + ['length', 'validate-length'] + ]; } public function testGetClassLength() { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + $attribute = $this->createAttribute(); + $attribute->method('getIsRequired') ->willReturn(true); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attribute->method('getFrontendClass') ->willReturn(''); - $attributeMock->expects($this->exactly(3)) + $attribute->expects($this->exactly(3)) ->method('getValidateRules') ->willReturn([ 'input_validation' => 'length', @@ -180,12 +183,31 @@ public function testGetClassLength() 'max_text_length' => 2, ]); - $this->model->setAttribute($attributeMock); + $this->model->setAttribute($attribute); $result = $this->model->getClass(); - $this->assertContains('minimum-length-1', $result); - $this->assertContains('maximum-length-2', $result); - $this->assertContains('validate-length', $result); + self::assertContains('minimum-length-1', $result); + self::assertContains('maximum-length-2', $result); + self::assertContains('validate-length', $result); + } + + /** + * Entity attribute factory. + * + * @return AbstractAttribute | MockObject + */ + private function createAttribute() + { + return $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getIsRequired', + 'getFrontendClass', + 'getValidateRules', + 'getAttributeCode', + 'getSource' + ]) + ->getMockForAbstractClass(); } public function testGetSelectOptions() @@ -196,33 +218,25 @@ public function testGetSelectOptions() $options = ['option1', 'option2']; $serializedOptions = "{['option1', 'option2']}"; - $this->storeManagerMock->expects($this->once()) - ->method('getStore') - ->willReturn($this->storeMock); - $this->storeMock->expects($this->once()) - ->method('getId') + $this->storeManager->method('getStore') + ->willReturn($this->store); + $this->store->method('getId') ->willReturn($storeId); - $this->attributeMock->expects($this->once()) - ->method('getAttributeCode') + $this->attribute->method('getAttributeCode') ->willReturn($attributeCode); - $this->cacheMock->expects($this->once()) - ->method('load') + $this->cache->method('load') ->with($cacheKey) ->willReturn(false); - $this->attributeMock->expects($this->once()) - ->method('getSource') - ->willReturn($this->sourceMock); - $this->sourceMock->expects($this->once()) - ->method('getAllOptions') + $this->attribute->method('getSource') + ->willReturn($this->source); + $this->source->method('getAllOptions') ->willReturn($options); - $this->serializerMock->expects($this->once()) - ->method('serialize') + $this->serializer->method('serialize') ->with($options) ->willReturn($serializedOptions); - $this->cacheMock->expects($this->once()) - ->method('save') + $this->cache->method('save') ->with($serializedOptions, $cacheKey, $this->cacheTags); - $this->assertSame($options, $this->model->getSelectOptions()); + self::assertSame($options, $this->model->getSelectOptions()); } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php index d4c91e98d9608..1584b922abaa9 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php @@ -40,6 +40,7 @@ protected function setUp() 'resource' => $this->resourceMock, 'translitFilter' => $translitFilter, 'context' => $contextMock, + 'reservedSystemNames' => ['configurable'], ]; $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( @@ -67,6 +68,8 @@ public function attributeGroupCodeDataProvider() { return [ ['General Group', 'general-group'], + ['configurable', md5('configurable')], + ['configurAble', md5('configurable')], ['///', md5('///')], ]; } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php index ee972c27aa8a2..8cf5df877a6eb 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php @@ -101,13 +101,13 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t1' => "table"], - 'condition' => - "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123' AND code_t1.store_id='0'", + 'condition' => "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123'" + . " AND code_t1.store_id='0'", ], 1 => [ 'requisites' => ['code_t2' => "table"], - 'condition' => - "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123' AND code_t2.store_id='12'", + 'condition' => "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123'" + . " AND code_t2.store_id='12'", ], ], 'expectedOrder' => 'IF(code_t2.value_id > 0, code_t2.value, code_t1.value) ASC', @@ -118,13 +118,13 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t1' => "table"], - 'condition' => - "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123' AND code_t1.store_id='0'", + 'condition' => "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123'" + . " AND code_t1.store_id='0'", ], 1 => [ 'requisites' => ['code_t2' => "table"], - 'condition' => - "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123' AND code_t2.store_id='12'", + 'condition' => "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123'" + . " AND code_t2.store_id='12'", ], ], 'expectedOrder' => 'IF(code_t2.value_id > 0, code_t2.value, code_t1.value) DESC', @@ -135,8 +135,8 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t' => "table"], - 'condition' => - "e.entity_id=code_t.entity_id AND code_t.attribute_id='123' AND code_t.store_id='0'", + 'condition' => "e.entity_id=code_t.entity_id AND code_t.attribute_id='123'" + . " AND code_t.store_id='0'", ], ], 'expectedOrder' => 'code_t.value DESC', @@ -147,8 +147,8 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t' => "table"], - 'condition' => - "e.entity_id=code_t.entity_id AND code_t.attribute_id='123' AND code_t.store_id='0'", + 'condition' => "e.entity_id=code_t.entity_id AND code_t.attribute_id='123'" + . " AND code_t.store_id='0'", ], ], 'expectedOrder' => 'code_t.value ASC', diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php index bc4ed7d4bd9e4..ab5b40c56685c 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Eav\Test\Unit\Model\Entity\Collection; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * AbstractCollection test * @@ -28,7 +31,7 @@ class AbstractCollectionTest extends \PHPUnit\Framework\TestCase protected $loggerMock; /** - * @var \Magento\Framework\Data\Collection\Db\FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $fetchStrategyMock; @@ -58,7 +61,7 @@ class AbstractCollectionTest extends \PHPUnit\Framework\TestCase protected $resourceHelperMock; /** - * @var \Magento\Framework\Validator\UniversalFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResourceModelPoolInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $validatorFactoryMock; diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/CodeTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/CodeTest.php new file mode 100644 index 0000000000000..9db290bcba3e1 --- /dev/null +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/CodeTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** + * Test for \Magento\Eav\Model\Validator\Attribute\Code + */ +namespace Magento\Eav\Test\Unit\Model\Validator\Attribute; + +use Magento\Eav\Model\Validator\Attribute\Code; +use PHPUnit\Framework\TestCase; + +/** + * Class CodeTest + */ +class CodeTest extends TestCase +{ + /** + * Testing \Magento\Eav\Model\Validator\Attribute\Code::isValid + * + * @dataProvider isValidDataProvider + * @param string $attributeCode + * @param bool $expected + * @throws \Zend_Validate_Exception + */ + public function testIsValid(string $attributeCode, bool $expected): void + { + $validator = new Code(); + $this->assertEquals($expected, $validator->isValid($attributeCode)); + } + + /** + * Data provider for testIsValid + * + * @return array + */ + public function isValidDataProvider(): array + { + return [ + [ + 'Attribute_code', + true + ], [ + 'attribute_1', + true + ],[ + 'Attribute_1', + true + ], [ + '_attribute_code', + false + ], [ + 'attribute.code', + false + ], [ + '1attribute_code', + false + ], [ + 'more_than_60_chars_more_than_60_chars_more_than_60_chars_more', + false + ] + ]; + } +} diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 8e897b979d2f0..db6f9b0a64f9f 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Eav\Model\Entity\Setup\PropertyMapperInterface" type="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite" /> <preference for="Magento\Eav\Model\Entity\AttributeLoaderInterface" type="Magento\Eav\Model\Entity\AttributeLoader" /> + <preference for="Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface" type="Magento\Eav\Model\Entity\Attribute\UniqueValidator" /> <preference for="Magento\Eav\Api\Data\AttributeInterface" type="Magento\Eav\Model\Entity\Attribute" /> <preference for="Magento\Eav\Api\AttributeRepositoryInterface" type="Magento\Eav\Model\AttributeRepository" /> <preference for="Magento\Eav\Api\Data\AttributeGroupInterface" type="Magento\Eav\Model\Entity\Attribute\Group" /> @@ -209,4 +210,3 @@ </arguments> </type> </config> - diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php index b7f21696162dd..26173fcf29b0c 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -19,13 +19,19 @@ class Converter implements ConverterInterface */ private const ES_NO_INDEX = false; + /** + * Text flags for Elasticsearch no analyze index value + */ + private const ES_NO_ANALYZE = false; + /** * Mapping between internal data types and elastic service. * * @var array */ private $mapping = [ - 'no_index' => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, ]; /** diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php index a6838d831b4bc..0ae347d5791ad 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php @@ -125,6 +125,7 @@ public function query(RequestInterface $request) [ 'documents' => $rawDocuments, 'aggregations' => $aggregationBuilder->build($request, $rawResponse), + 'total' => isset($rawResponse['hits']['total']) ? $rawResponse['hits']['total'] : 0 ] ); return $queryResponse; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php index db961d86962e9..09968db00aa25 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\RequestInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -12,6 +15,8 @@ use Magento\Framework\App\ScopeResolverInterface; /** + * Query builder for search adapter. + * * @api * @since 100.1.0 */ @@ -41,22 +46,30 @@ class Builder */ protected $scopeResolver; + /** + * @var Sort + */ + protected $sortBuilder; + /** * @param Config $clientConfig * @param SearchIndexNameResolver $searchIndexNameResolver * @param AggregationBuilder $aggregationBuilder * @param ScopeResolverInterface $scopeResolver + * @param Sort|null $sortBuilder */ public function __construct( Config $clientConfig, SearchIndexNameResolver $searchIndexNameResolver, AggregationBuilder $aggregationBuilder, - ScopeResolverInterface $scopeResolver + ScopeResolverInterface $scopeResolver, + Sort $sortBuilder = null ) { $this->clientConfig = $clientConfig; $this->searchIndexNameResolver = $searchIndexNameResolver; $this->aggregationBuilder = $aggregationBuilder; $this->scopeResolver = $scopeResolver; + $this->sortBuilder = $sortBuilder ?: ObjectManager::getInstance()->get(Sort::class); } /** @@ -70,6 +83,7 @@ public function initQuery(RequestInterface $request) { $dimension = current($request->getDimensions()); $storeId = $this->scopeResolver->getScope($dimension->getValue())->getId(); + $searchQuery = [ 'index' => $this->searchIndexNameResolver->getIndexName($storeId, $request->getIndex()), 'type' => $this->clientConfig->getEntityType(), @@ -77,6 +91,7 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'stored_fields' => ['_id', '_score'], + 'sort' => $this->sortBuilder->getSort($request), 'query' => [], ], ]; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php index 875d384a20596..56c84593256be 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php @@ -72,13 +72,15 @@ public function __construct( */ public function getFields(array $productIds, $storeId) { + $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + $priceData = $this->dataProvider->getSearchableAttribute('price') ? $this->resourceIndex->getPriceIndexData($productIds, $storeId) : []; $fields = []; foreach ($productIds as $productId) { - $fields[$productId] = $this->getProductPriceData($productId, $storeId, $priceData); + $fields[$productId] = $this->getProductPriceData($productId, $websiteId, $priceData); } return $fields; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index e4f5de46c4c86..270ca37e2d42c 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -12,12 +12,18 @@ use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface; use Magento\Elasticsearch\Model\Adapter\FieldType\Date as DateFieldType; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; /** * Map product index data to search engine metadata */ class ProductDataMapper implements BatchDataMapperInterface { + /** + * @var AttributeOptionInterface[] + */ + private $attributeOptionsCache; + /** * @var Builder */ @@ -95,6 +101,7 @@ public function __construct( $this->excludedAttributes = array_merge($this->defaultExcludedAttributes, $excludedAttributes); $this->additionalFieldsProvider = $additionalFieldsProvider; $this->dataProvider = $dataProvider; + $this->attributeOptionsCache = []; } /** @@ -272,7 +279,13 @@ private function isAttributeDate(Attribute $attribute): bool private function getValuesLabels(Attribute $attribute, array $attributeValues): array { $attributeLabels = []; - foreach ($attribute->getOptions() as $option) { + + $options = $this->getAttributeOptions($attribute); + if (empty($options)) { + return $attributeLabels; + } + + foreach ($options as $option) { if (\in_array($option->getValue(), $attributeValues)) { $attributeLabels[] = $option->getLabel(); } @@ -281,6 +294,22 @@ private function getValuesLabels(Attribute $attribute, array $attributeValues): return $attributeLabels; } + /** + * Retrieve options for attribute + * + * @param Attribute $attribute + * @return array + */ + private function getAttributeOptions(Attribute $attribute): array + { + if (!isset($this->attributeOptionsCache[$attribute->getId()])) { + $options = $attribute->getOptions() ?? []; + $this->attributeOptionsCache[$attribute->getId()] = $options; + } + + return $this->attributeOptionsCache[$attribute->getId()]; + } + /** * Retrieve value for field. If field have only one value this method return it. * Otherwise will be returned array of these values. diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index 17ebf9c83903e..54586fa357ff0 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -146,6 +146,16 @@ public function getAttributeCode(): string return $this->attributeCode; } + /** + * Check if attribute is sortable. + * + * @return bool + */ + public function isSortable(): bool + { + return (int)$this->getAttribute()->getUsedForSortBy() === 1; + } + /** * Check if attribute is defined by user. * diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php index b8c0da53c53e0..19b9f85c44b03 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php @@ -59,4 +59,24 @@ public function setCustomAttributes(array $attributes) { return $this; } + + /** + * Get property value that guarantee of using an attribute in sort purposes on the storefront. + * + * @return bool + */ + public function getUsedForSortBy() + { + return false; + } + + /** + * Dummy attribute doesn't have backend type. + * + * @return null + */ + public function getBackendType() + { + return null; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php index c7e2a4beabb5c..268fe00e4c41e 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -18,6 +18,8 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\ObjectManager; /** * Provide dynamic fields for product. @@ -27,10 +29,18 @@ class DynamicField implements FieldProviderInterface /** * Category list. * + * @deprecated * @var CategoryListInterface */ private $categoryList; + /** + * Category collection. + * + * @var Collection + */ + private $categoryCollection; + /** * Customer group repository. * @@ -73,6 +83,7 @@ class DynamicField implements FieldProviderInterface * @param CategoryListInterface $categoryList * @param FieldNameResolver $fieldNameResolver * @param AttributeProvider $attributeAdapterProvider + * @param Collection|null $categoryCollection */ public function __construct( FieldTypeConverterInterface $fieldTypeConverter, @@ -81,7 +92,8 @@ public function __construct( SearchCriteriaBuilder $searchCriteriaBuilder, CategoryListInterface $categoryList, FieldNameResolver $fieldNameResolver, - AttributeProvider $attributeAdapterProvider + AttributeProvider $attributeAdapterProvider, + Collection $categoryCollection = null ) { $this->groupRepository = $groupRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; @@ -90,6 +102,8 @@ public function __construct( $this->categoryList = $categoryList; $this->fieldNameResolver = $fieldNameResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->categoryCollection = $categoryCollection ?: + ObjectManager::getInstance()->get(Collection::class); } /** @@ -98,18 +112,17 @@ public function __construct( public function getFields(array $context = []): array { $allAttributes = []; - $searchCriteria = $this->searchCriteriaBuilder->create(); - $categories = $this->categoryList->getList($searchCriteria)->getItems(); + $categoryIds = $this->categoryCollection->getAllIds(); $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); - foreach ($categories as $category) { + foreach ($categoryIds as $categoryId) { $categoryPositionKey = $this->fieldNameResolver->getFieldName( $positionAttribute, - ['categoryId' => $category->getId()] + ['categoryId' => $categoryId] ); $categoryNameKey = $this->fieldNameResolver->getFieldName( $categoryNameAttribute, - ['categoryId' => $category->getId()] + ['categoryId' => $categoryId] ); $allAttributes[$categoryPositionKey] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), @@ -121,12 +134,15 @@ public function getFields(array $context = []): array ]; } + $searchCriteria = $this->searchCriteriaBuilder->create(); $groups = $this->groupRepository->getList($searchCriteria)->getItems(); $priceAttribute = $this->attributeAdapterProvider->getByAttributeCode('price'); + $ctx = isset($context['websiteId']) ? ['websiteId' => $context['websiteId']] : []; foreach ($groups as $group) { + $ctx['customerGroupId'] = $group->getId(); $groupPriceKey = $this->fieldNameResolver->getFieldName( $priceAttribute, - ['customerGroupId' => $group->getId(), 'websiteId' => $context['websiteId']] + $ctx ); $allAttributes[$groupPriceKey] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_FLOAT), diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php index 5abe4972e34d0..535ab62dd5991 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -17,13 +17,19 @@ class Converter implements ConverterInterface */ private const ES_NO_INDEX = 'no'; + /** + * Text flags for Elasticsearch no analyze index value + */ + private const ES_NO_ANALYZE = 'not_analyzed'; + /** * Mapping between internal data types and elastic service. * * @var array */ private $mapping = [ - 'no_index' => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, ]; /** diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php index 02c4d29558dad..5ecfd62430032 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php @@ -17,6 +17,7 @@ interface ConverterInterface */ public const INTERNAL_NO_INDEX_VALUE = 'no_index'; public const INTERNAL_INDEX_VALUE = 'index'; + public const INTERNAL_NO_ANALYZE_VALUE = 'no_analyze'; /** * Get service field index type. diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php index 3208ca7fc6171..5f319daeb3b39 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -51,22 +51,22 @@ public function __construct( */ public function getFieldName(AttributeAdapter $attribute, $context = []): ?string { - $fieldType = $this->fieldTypeResolver->getFieldType($attribute); $attributeCode = $attribute->getAttributeCode(); - $frontendInput = $attribute->getFrontendInput(); if (empty($context['type'])) { - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_FILTER) { + return $attributeCode; + } + + $fieldType = $this->fieldTypeResolver->getFieldType($attribute); + $frontendInput = $attribute->getFrontendInput(); + $fieldName = $attributeCode; + if ($context['type'] === FieldMapperInterface::TYPE_FILTER) { if ($this->isStringServiceFieldType($fieldType)) { - return $this->getFieldName( - $attribute, - array_merge($context, ['type' => FieldMapperInterface::TYPE_QUERY]) - ); + return $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); } $fieldName = $attributeCode; } elseif ($context['type'] === FieldMapperInterface::TYPE_QUERY) { $fieldName = $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); - } else { + } elseif ($context['type'] == FieldMapperInterface::TYPE_SORT && $attribute->isSortable()) { $fieldName = 'sort_' . $attributeCode; } @@ -115,10 +115,11 @@ private function getQueryTypeFieldName($frontendInput, $fieldType, $attributeCod private function getRefinedFieldName($frontendInput, $fieldType, $attributeCode) { $stringTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING); + $keywordTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD); switch ($frontendInput) { case 'select': case 'multiselect': - return in_array($fieldType, [$stringTypeKey, 'integer'], true) + return in_array($fieldType, [$stringTypeKey, $keywordTypeKey, 'integer'], true) ? $attributeCode . '_value' : $attributeCode; case 'boolean': diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php index 4fa16db98639e..652fc853adbcc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php @@ -24,7 +24,9 @@ class SpecialAttribute implements ResolverInterface */ public function getFieldName(AttributeAdapter $attribute, $context = []): ?string { - if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true)) { + if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true) + && empty($context['type']) + ) { return $attribute->getAttributeCode(); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index b5c78cbc28f45..6876b23bbb156 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -7,6 +7,7 @@ namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider; +use Magento\Framework\App\ObjectManager; use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; @@ -19,6 +20,7 @@ as FieldTypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface as FieldIndexResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; /** * Provide static fields for mapping of product. @@ -55,6 +57,11 @@ class StaticField implements FieldProviderInterface */ private $fieldIndexResolver; + /** + * @var FieldName\ResolverInterface + */ + private $fieldNameResolver; + /** * @param Config $eavConfig * @param FieldTypeConverterInterface $fieldTypeConverter @@ -62,6 +69,7 @@ class StaticField implements FieldProviderInterface * @param FieldTypeResolver $fieldTypeResolver * @param FieldIndexResolver $fieldIndexResolver * @param AttributeProvider $attributeAdapterProvider + * @param FieldName\ResolverInterface|null $fieldNameResolver */ public function __construct( Config $eavConfig, @@ -69,7 +77,8 @@ public function __construct( IndexTypeConverterInterface $indexTypeConverter, FieldTypeResolver $fieldTypeResolver, FieldIndexResolver $fieldIndexResolver, - AttributeProvider $attributeAdapterProvider + AttributeProvider $attributeAdapterProvider, + FieldName\ResolverInterface $fieldNameResolver = null ) { $this->eavConfig = $eavConfig; $this->fieldTypeConverter = $fieldTypeConverter; @@ -77,6 +86,8 @@ public function __construct( $this->fieldTypeResolver = $fieldTypeResolver; $this->fieldIndexResolver = $fieldIndexResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(FieldName\ResolverInterface::class); } /** @@ -84,6 +95,7 @@ public function __construct( * * @param array $context * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getFields(array $context = []): array { @@ -92,19 +104,38 @@ public function getFields(array $context = []): array foreach ($attributes as $attribute) { $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); - $code = $attributeAdapter->getAttributeCode(); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); - $allAttributes[$code] = [ + $allAttributes[$fieldName] = [ 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), ]; $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); if (null !== $index) { - $allAttributes[$code]['index'] = $index; + $allAttributes[$fieldName]['index'] = $index; + } + + if ($attributeAdapter->isSortable()) { + $sortFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $allAttributes[$fieldName]['fields'][$sortFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ), + 'index' => $this->indexTypeConverter->convert( + IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE + ) + ]; } if ($attributeAdapter->isComplexType()) { - $allAttributes[$code . '_value'] = [ + $childFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + $allAttributes[$childFieldName] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) ]; } diff --git a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php new file mode 100644 index 0000000000000..b3f8a56110f8d --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; +use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyInterface; + +/** + * Strategy interface for preparing product collection. + */ +class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface +{ + /** + * @var Config + */ + private $catalogConfig; + + /** + * @param Config $catalogConfig + */ + public function __construct( + Config $catalogConfig + ) { + $this->catalogConfig = $catalogConfig; + } + + /** + * @inheritdoc + */ + public function prepare(Collection $collection) + { + $collection + ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) + ->addMinimalPrice() + ->addTaxPercents(); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Config.php b/app/code/Magento/Elasticsearch/Model/Config.php index dc08a72a9feb3..387db07c62f90 100644 --- a/app/code/Magento/Elasticsearch/Model/Config.php +++ b/app/code/Magento/Elasticsearch/Model/Config.php @@ -25,8 +25,6 @@ class Config implements ClientOptionsInterface */ const ENGINE_NAME = 'elasticsearch'; - private const ENGINE_NAME_5 = 'elasticsearch5'; - /** * Elasticsearch Entity type */ @@ -64,23 +62,31 @@ class Config implements ClientOptionsInterface private $engineResolver; /** - * Constructor + * Available Elasticsearch engines. * + * @var array + */ + private $engineList; + + /** * @param ScopeConfigInterface $scopeConfig * @param ClientResolver|null $clientResolver * @param EngineResolverInterface|null $engineResolver * @param string|null $prefix + * @param array $engineList */ public function __construct( ScopeConfigInterface $scopeConfig, ClientResolver $clientResolver = null, EngineResolverInterface $engineResolver = null, - $prefix = null + $prefix = null, + $engineList = [] ) { $this->scopeConfig = $scopeConfig; $this->clientResolver = $clientResolver ?: ObjectManager::getInstance()->get(ClientResolver::class); $this->engineResolver = $engineResolver ?: ObjectManager::getInstance()->get(EngineResolverInterface::class); $this->prefix = $prefix ?: $this->clientResolver->getCurrentEngine(); + $this->engineList = $engineList; } /** @@ -138,7 +144,7 @@ public function getSearchConfigData($field, $storeId = null) */ public function isElasticsearchEnabled() { - return in_array($this->engineResolver->getCurrentSearchEngine(), [self::ENGINE_NAME, self::ENGINE_NAME_5]); + return in_array($this->engineResolver->getCurrentSearchEngine(), $this->engineList); } /** diff --git a/app/code/Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider.php b/app/code/Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider.php new file mode 100644 index 0000000000000..ef2992e1fff9f --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Layer/Category/ItemCollectionProvider.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\Layer\Category; + +use Magento\Catalog\Model\Layer\ItemCollectionProviderInterface; +use Magento\Framework\Search\EngineResolverInterface; + +/** + * Catalog search category layer collection provider. + */ +class ItemCollectionProvider implements ItemCollectionProviderInterface +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $factories; + + /** + * ItemCollectionProvider constructor. + * @param EngineResolverInterface $engineResolver + * @param array $factories + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $factories + ) { + $this->engineResolver = $engineResolver; + $this->factories = $factories; + } + + /** + * @inheritdoc + */ + public function getCollection(\Magento\Catalog\Model\Category $category) + { + if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + } + $collection = $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); + $collection->addCategoryFilter($category); + + return $collection; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php b/app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php new file mode 100644 index 0000000000000..7d2a30b493d30 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\Layer\Search; + +use Magento\Catalog\Model\Layer\ItemCollectionProviderInterface; +use Magento\Framework\Search\EngineResolverInterface; + +/** + * Catalog search category layer collection provider. + */ +class ItemCollectionProvider implements ItemCollectionProviderInterface +{ + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $factories; + + /** + * ItemCollectionProvider constructor. + * @param EngineResolverInterface $engineResolver + * @param array $factories + */ + public function __construct( + EngineResolverInterface $engineResolver, + array $factories + ) { + $this->engineResolver = $engineResolver; + $this->factories = $factories; + } + + /** + * @inheritdoc + */ + public function getCollection(\Magento\Catalog\Model\Category $category) + { + if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { + throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + } + $collection = $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); + + return $collection; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php new file mode 100644 index 0000000000000..32cb85ff750c4 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchCriteriaResolver implements SearchCriteriaResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $builder; + + /** + * @var Collection + */ + private $collection; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var int + */ + private $size; + + /** + * @var array + */ + private $orders; + + /** + * @var int + */ + private $currentPage; + + /** + * SearchCriteriaResolver constructor. + * @param SearchCriteriaBuilder $builder + * @param Collection $collection + * @param string $searchRequestName + * @param int $currentPage + * @param int $size + * @param array $orders + */ + public function __construct( + SearchCriteriaBuilder $builder, + Collection $collection, + string $searchRequestName, + int $currentPage, + int $size, + ?array $orders + ) { + $this->builder = $builder; + $this->collection = $collection; + $this->searchRequestName = $searchRequestName; + $this->currentPage = $currentPage; + $this->size = $size; + $this->orders = $orders; + } + + /** + * @inheritdoc + */ + public function resolve(): SearchCriteria + { + $this->builder->setPageSize($this->size); + $searchCriteria = $this->builder->create(); + $searchCriteria->setRequestName($this->searchRequestName); + $searchCriteria->setSortOrders(array_merge(['relevance' => 'DESC'], $this->orders)); + $searchCriteria->setCurrentPage($this->currentPage - 1); + + return $searchCriteria; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php new file mode 100644 index 0000000000000..3ae2d384782c3 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchResultInterface; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchResultApplier implements SearchResultApplierInterface +{ + /** + * @var Collection|\Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection + */ + private $collection; + + /** + * @var SearchResultInterface + */ + private $searchResult; + + /** + * @param Collection $collection + * @param SearchResultInterface $searchResult + */ + public function __construct( + Collection $collection, + SearchResultInterface $searchResult + ) { + $this->collection = $collection; + $this->searchResult = $searchResult; + } + + /** + * @inheritdoc + */ + public function apply() + { + if (empty($this->searchResult->getItems())) { + $this->collection->getSelect()->where('NULL'); + return; + } + $ids = []; + foreach ($this->searchResult->getItems() as $item) { + $ids[] = (int)$item->getId(); + } + $this->collection->setPageSize(null); + $this->collection->getSelect()->where('e.entity_id IN (?)', $ids); + $orderList = join(',', $ids); + $this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER); + $this->collection->getSelect()->order("FIELD(e.entity_id,$orderList)"); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php new file mode 100644 index 0000000000000..109721fcc71a9 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/TotalRecordsResolver.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\Framework\Api\Search\SearchResultInterface; + +/** + * Resolve total records count. + */ +class TotalRecordsResolver implements TotalRecordsResolverInterface +{ + /** + * @var SearchResultInterface + */ + private $searchResult; + + /** + * @param SearchResultInterface $searchResult + */ + public function __construct( + SearchResultInterface $searchResult + ) { + $this->searchResult = $searchResult; + } + + /** + * @inheritdoc + */ + public function resolve(): ?int + { + return $this->searchResult->getTotalCount(); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php index 43b2bfe553a98..6f9ef552351fd 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php @@ -68,8 +68,7 @@ public function __construct( } /** - * @param RequestInterface $request - * @return QueryResponse + * @inheritdoc */ public function query(RequestInterface $request) { @@ -86,6 +85,7 @@ public function query(RequestInterface $request) [ 'documents' => $rawDocuments, 'aggregations' => $aggregationBuilder->build($request, $rawResponse), + 'total' => isset($rawResponse['hits']['total']) ? $rawResponse['hits']['total'] : 0 ] ); return $queryResponse; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php index bcfb7f5565b86..eeb48f805bccf 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php @@ -8,10 +8,13 @@ use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; use Magento\Framework\Search\Dynamic\DataProviderInterface; +/** + * Builder for term buckets. + */ class Term implements BucketBuilderInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -19,13 +22,15 @@ public function build( array $queryResult, DataProviderInterface $dataProvider ) { + $buckets = $queryResult['aggregations'][$bucket->getName()]['buckets'] ?? []; $values = []; - foreach ($queryResult['aggregations'][$bucket->getName()]['buckets'] as $resultBucket) { + foreach ($buckets as $resultBucket) { $values[$resultBucket['key']] = [ 'value' => $resultBucket['key'], 'count' => $resultBucket['doc_count'], ]; } + return $values; } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index cfdab2463311e..496a77e4c5ac3 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -212,6 +212,7 @@ public function getAggregation( 'histogram' => [ 'field' => $fieldName, 'interval' => (float)$range, + 'min_doc_count' => 1, ], ], ]; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php index cf75366b9b25e..d0aaa4b3dd572 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php @@ -3,19 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\SearchAdapter\Query; use Magento\Framework\Search\RequestInterface; -use Magento\Framework\App\ScopeResolverInterface; use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query\Builder as Elasticsearch5Builder; /** + * Query builder for search adapter. + * * @api * @since 100.1.0 */ class Builder extends Elasticsearch5Builder { - /** * Set initial settings for query * @@ -34,6 +35,7 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'fields' => ['_id', '_score'], + 'sort' => $this->sortBuilder->getSort($request), 'query' => [], ], ]; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php index f1c3451482bab..afd383c13421f 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -5,11 +5,18 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface as TypeResolver; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Query\BoolExpression; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; +/** + * Builder for match query. + */ class Match implements QueryInterface { /** @@ -23,24 +30,53 @@ class Match implements QueryInterface private $fieldMapper; /** + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @var PreprocessorInterface[] */ protected $preprocessorContainer; + /** + * @var AttributeProvider + */ + private $attributeProvider; + + /** + * @var TypeResolver + */ + private $fieldTypeResolver; + + /** + * @var ValueTransformerPool + */ + private $valueTransformerPool; + /** * @param FieldMapperInterface $fieldMapper * @param PreprocessorInterface[] $preprocessorContainer + * @param AttributeProvider|null $attributeProvider + * @param TypeResolver|null $fieldTypeResolver + * @param ValueTransformerPool|null $valueTransformerPool */ public function __construct( FieldMapperInterface $fieldMapper, - array $preprocessorContainer + array $preprocessorContainer, + AttributeProvider $attributeProvider = null, + TypeResolver $fieldTypeResolver = null, + ValueTransformerPool $valueTransformerPool = null ) { $this->fieldMapper = $fieldMapper; $this->preprocessorContainer = $preprocessorContainer; + $this->attributeProvider = $attributeProvider ?? ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldTypeResolver = $fieldTypeResolver ?? ObjectManager::getInstance() + ->get(TypeResolver::class); + $this->valueTransformerPool = $valueTransformerPool ?? ObjectManager::getInstance() + ->get(ValueTransformerPool::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $conditionType) { @@ -61,16 +97,14 @@ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $ } /** + * Prepare query. + * * @param string $queryValue * @param string $conditionType * @return array */ protected function prepareQuery($queryValue, $conditionType) { - $queryValue = $this->escape($queryValue); - foreach ($this->preprocessorContainer as $preprocessor) { - $queryValue = $preprocessor->process($queryValue); - } $condition = $conditionType === BoolExpression::QUERY_CONDITION_NOT ? self::QUERY_CONDITION_MUST_NOT : $conditionType; return [ @@ -99,10 +133,24 @@ protected function buildQueries(array $matches, array $queryValue) // Checking for quoted phrase \"phrase test\", trim escaped surrounding quotes if found $count = 0; - $value = preg_replace('#^\\\\"(.*)\\\\"$#m', '$1', $queryValue['value'], -1, $count); + $value = preg_replace('#^"(.*)"$#m', '$1', $queryValue['value'], -1, $count); $condition = ($count) ? 'match_phrase' : 'match'; + $transformedTypes = []; foreach ($matches as $match) { + $attributeAdapter = $this->attributeProvider->getByAttributeCode($match['field']); + $fieldType = $this->fieldTypeResolver->getFieldType($attributeAdapter); + $valueTransformer = $this->valueTransformerPool->get($fieldType ?? 'text'); + $valueTransformerHash = \spl_object_hash($valueTransformer); + if (!isset($transformedTypes[$valueTransformerHash])) { + $transformedTypes[$valueTransformerHash] = $valueTransformer->transform($value); + } + $transformedValue = $transformedTypes[$valueTransformerHash]; + if (null === $transformedValue) { + //Value is incompatible with this field type. + continue; + } + $resolvedField = $this->fieldMapper->getFieldName( $match['field'], ['type' => FieldMapperInterface::TYPE_QUERY] @@ -112,8 +160,8 @@ protected function buildQueries(array $matches, array $queryValue) 'body' => [ $condition => [ $resolvedField => [ - 'query' => $value, - 'boost' => isset($match['boost']) ? $match['boost'] : 1, + 'query' => $transformedValue, + 'boost' => $match['boost'] ?? 1, ], ], ], @@ -124,18 +172,15 @@ protected function buildQueries(array $matches, array $queryValue) } /** - * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. * + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @param string $value * @return string */ protected function escape($value) { - $value = preg_replace('/@+|[@+-]+$/', '', $value); - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; $replace = '\\\$1'; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php new file mode 100644 index 0000000000000..5ccf202e3812b --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Framework\Search\RequestInterface; + +/** + * Sort builder. + */ +class Sort +{ + /** + * List of fields that need to skipp by default. + */ + private const DEFAULT_SKIPPED_FIELDS = [ + 'entity_id', + ]; + + /** + * Default mapping for special fields. + */ + private const DEFAULT_MAP = [ + 'relevance' => '_score', + ]; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var array + */ + private $skippedFields; + + /** + * @var array + */ + private $map; + + /** + * @param AttributeProvider $attributeAdapterProvider + * @param FieldNameResolver $fieldNameResolver + * @param array $skippedFields + * @param array $map + */ + public function __construct( + AttributeProvider $attributeAdapterProvider, + FieldNameResolver $fieldNameResolver, + array $skippedFields = [], + array $map = [] + ) { + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver; + $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); + $this->map = array_merge(self::DEFAULT_MAP, $map); + } + + /** + * Prepare sort. + * + * @param RequestInterface $request + * @return array + */ + public function getSort(RequestInterface $request) + { + $sorts = []; + foreach ($request->getSort() as $item) { + if (in_array($item['field'], $this->skippedFields)) { + continue; + } + $attribute = $this->attributeAdapterProvider->getByAttributeCode($item['field']); + $fieldName = $this->fieldNameResolver->getFieldName($attribute); + if (isset($this->map[$fieldName])) { + $fieldName = $this->map[$fieldName]; + } + if ($attribute->isSortable() && !($attribute->isFloatType() || $attribute->isIntegerType())) { + $suffix = $this->fieldNameResolver->getFieldName( + $attribute, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldName .= '.' . $suffix; + } + $sorts[] = [ + $fieldName => [ + 'order' => strtolower($item['direction']) + ] + ]; + } + + return $sorts; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php new file mode 100644 index 0000000000000..49eca6e9d82a6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\Model\Adapter\FieldType\Date; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for date type fields. + */ +class DateTransformer implements ValueTransformerInterface +{ + /** + * @var Date + */ + private $dateFieldType; + + /** + * @param Date $dateFieldType + */ + public function __construct(Date $dateFieldType) + { + $this->dateFieldType = $dateFieldType; + } + + /** + * @inheritdoc + */ + public function transform(string $value): ?string + { + try { + $formattedDate = $this->dateFieldType->formatDate(null, $value); + } catch (\Exception $e) { + $formattedDate = null; + } + + return $formattedDate; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php new file mode 100644 index 0000000000000..5e330076d3df7 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for float type fields. + */ +class FloatTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?float + { + return \is_numeric($value) ? (float) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php new file mode 100644 index 0000000000000..0846ff3a9bd86 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for integer type fields. + */ +class IntegerTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?int + { + return \is_numeric($value) ? (int) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php new file mode 100644 index 0000000000000..68bec2580f621 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; + +/** + * Value transformer for fields with text types. + */ +class TextTransformer implements ValueTransformerInterface +{ + /** + * @var PreprocessorInterface[] + */ + private $preprocessors; + + /** + * @param PreprocessorInterface[] $preprocessors + */ + public function __construct(array $preprocessors = []) + { + foreach ($preprocessors as $preprocessor) { + if (!$preprocessor instanceof PreprocessorInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($preprocessor)) + ); + } + } + + $this->preprocessors = $preprocessors; + } + + /** + * @inheritdoc + */ + public function transform(string $value): string + { + $value = $this->escape($value); + foreach ($this->preprocessors as $preprocessor) { + $value = $preprocessor->process($value); + } + + return $value; + } + + /** + * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. + * + * @param string $value + * @return string + */ + private function escape(string $value): string + { + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $replace = '\\\$1'; + + return preg_replace($pattern, $replace, $value); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php new file mode 100644 index 0000000000000..c84ddc69cc7a8 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Value transformer of search term for matching with ES field types. + */ +interface ValueTransformerInterface +{ + /** + * Transform value according to field type. + * + * @param string $value + * @return mixed + */ + public function transform(string $value); +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php new file mode 100644 index 0000000000000..11a35d79ce1fd --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Pool of value transformers. + */ +class ValueTransformerPool +{ + /** + * @var ValueTransformerInterface[] + */ + private $transformers; + + /** + * @param ValueTransformerInterface[] $valueTransformers + */ + public function __construct(array $valueTransformers = []) + { + foreach ($valueTransformers as $valueTransformer) { + if (!$valueTransformer instanceof ValueTransformerInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($valueTransformer)) + ); + } + } + + $this->transformers = $valueTransformers; + } + + /** + * Get value transformer related to field type. + * + * @param string $fieldType + * @return ValueTransformerInterface + */ + public function get(string $fieldType): ValueTransformerInterface + { + return $this->transformers[$fieldType] ?? $this->transformers['default']; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php index 33fda48f4af57..0813975ac9a4b 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php @@ -76,6 +76,7 @@ public function create($response) [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => $response['total'] ] ); } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index ba5e97aa14b54..7c2a33c05aa08 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -24,6 +24,7 @@ use Magento\Customer\Api\Data\GroupInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; +use Magento\Catalog\Model\ResourceModel\Category\Collection; /** * @SuppressWarnings(PHPMD) @@ -65,6 +66,11 @@ class DynamicFieldTest extends \PHPUnit\Framework\TestCase */ private $categoryList; + /** + * @var Collection + */ + private $categoryCollection; + /** * @var FieldNameResolver */ @@ -100,6 +106,10 @@ protected function setUp() $this->categoryList = $this->getMockBuilder(CategoryListInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->categoryCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getAllIds']) + ->getMock(); $objectManager = new ObjectManagerHelper($this); @@ -113,6 +123,7 @@ protected function setUp() 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'categoryList' => $this->categoryList, 'fieldNameResolver' => $this->fieldNameResolver, + 'categoryCollection' => $this->categoryCollection, ] ); } @@ -124,7 +135,6 @@ protected function setUp() * @param $groupId * @param array $expected * @return void - * @throws \Magento\Framework\Exception\LocalizedException */ public function testGetAllAttributesTypes( $complexType, @@ -138,10 +148,6 @@ public function testGetAllAttributesTypes( $this->searchCriteriaBuilder->expects($this->any()) ->method('create') ->willReturn($searchCriteria); - $categorySearchResults = $this->getMockBuilder(CategorySearchResultsInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getItems']) - ->getMockForAbstractClass(); $groupSearchResults = $this->getMockBuilder(GroupSearchResultsInterface::class) ->disableOriginalConstructor() ->setMethods(['getItems']) @@ -156,19 +162,10 @@ public function testGetAllAttributesTypes( $groupSearchResults->expects($this->any()) ->method('getItems') ->willReturn([$group]); - $category = $this->getMockBuilder(CategoryInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getId']) - ->getMockForAbstractClass(); - $category->expects($this->any()) - ->method('getId') - ->willReturn($categoryId); - $categorySearchResults->expects($this->any()) - ->method('getItems') - ->willReturn([$category]); - $this->categoryList->expects($this->any()) - ->method('getList') - ->willReturn($categorySearchResults); + + $this->categoryCollection->expects($this->any()) + ->method('getAllIds') + ->willReturn([$categoryId]); $categoryAttributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php index 4fa99f3bf834d..fd5c87bc9518b 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php @@ -66,6 +66,7 @@ protected function setUp() * @param $fieldType * @param $attributeCode * @param $frontendInput + * @param $isSortable * @param $context * @param $expected * @return void @@ -74,6 +75,7 @@ public function testGetFieldName( $fieldType, $attributeCode, $frontendInput, + $isSortable, $context, $expected ) { @@ -82,7 +84,7 @@ public function testGetFieldName( ->willReturn('string'); $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getFrontendInput']) + ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('getAttributeCode') @@ -90,6 +92,9 @@ public function testGetFieldName( $attributeMock->expects($this->any()) ->method('getFrontendInput') ->willReturn($frontendInput); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $this->fieldTypeResolver->expects($this->any()) ->method('getFieldType') ->willReturn($fieldType); @@ -106,13 +111,13 @@ public function testGetFieldName( public function getFieldNameProvider() { return [ - ['', 'code', '', [], 'code'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['string', '*', '', ['type' => 'default'], '_all'], - ['', 'code', '', ['type' => 'default'], 'code'], - ['', 'code', 'select', ['type' => 'default'], 'code'], - ['', 'code', 'boolean', ['type' => 'default'], 'code'], - ['', 'code', '', ['type' => 'type'], 'sort_code'], + ['', 'code', '', false, [], 'code'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['string', '*', '', false, ['type' => 'default'], '_all'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['', 'code', 'select', false, ['type' => 'default'], 'code'], + ['', 'code', 'boolean', false, ['type' => 'default'], 'code'], + ['', 'code', '', true, ['type' => 'sort'], 'sort_code'], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php index bf8b601ed43ab..de85b8b6602b8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -20,6 +20,8 @@ as FieldTypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface as FieldIndexResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; /** * @SuppressWarnings(PHPMD) @@ -61,6 +63,11 @@ class StaticFieldTest extends \PHPUnit\Framework\TestCase */ private $fieldTypeResolver; + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + /** * Set up test environment * @@ -90,6 +97,10 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getFieldIndex']) ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); $objectManager = new ObjectManagerHelper($this); @@ -102,6 +113,7 @@ protected function setUp() 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'fieldIndexResolver' => $this->fieldIndexResolver, 'fieldTypeResolver' => $this->fieldTypeResolver, + 'fieldNameResolver' => $this->fieldNameResolver, ] ); } @@ -113,6 +125,10 @@ protected function setUp() * @param $indexType * @param $isComplexType * @param $complexType + * @param $isSortable + * @param $fieldName + * @param $compositeFieldName + * @param $sortFieldName * @param array $expected * @return void */ @@ -122,6 +138,10 @@ public function testGetAllAttributesTypes( $indexType, $isComplexType, $complexType, + $isSortable, + $fieldName, + $compositeFieldName, + $sortFieldName, $expected ) { $this->fieldTypeResolver->expects($this->any()) @@ -132,7 +152,30 @@ public function testGetAllAttributesTypes( ->willReturn($indexType); $this->indexTypeConverter->expects($this->any()) ->method('convert') - ->willReturn('no'); + ->with($this->anything()) + ->will($this->returnCallback( + function ($type) { + if ($type === 'no_index') { + return 'no'; + } elseif ($type === 'no_analyze') { + return 'not_analyzed'; + } + } + )); + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->with($this->anything()) + ->will($this->returnCallback( + function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return $sortFieldName; + } elseif ($context['type'] === 'text') { + return $compositeFieldName; + } + } + )); $productAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->setMethods(['getAttributeCode']) @@ -146,11 +189,14 @@ public function testGetAllAttributesTypes( $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'getAttributeCode']) + ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') ->willReturn($isComplexType); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); $attributeMock->expects($this->any()) ->method('getAttributeCode') ->willReturn($attributeCode); @@ -166,13 +212,12 @@ function ($type) use ($complexType) { static $callCount = []; $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; - } if ($type === 'string') { return 'string'; } elseif ($type === 'float') { return 'float'; + } elseif ($type === 'keyword') { + return 'string'; } else { return $complexType; } @@ -197,6 +242,10 @@ public function attributeProvider() true, true, 'text', + false, + 'category_ids', + 'category_ids_value', + '', [ 'category_ids' => [ 'type' => 'select', @@ -217,6 +266,10 @@ public function attributeProvider() 'no', false, null, + false, + 'attr_code', + '', + '', [ 'attr_code' => [ 'type' => 'text', @@ -234,6 +287,10 @@ public function attributeProvider() null, false, null, + false, + 'attr_code', + '', + '', [ 'attr_code' => [ 'type' => 'text' @@ -243,6 +300,32 @@ public function attributeProvider() 'index' => 'no' ] ] + ], + [ + 'attr_code', + 'text', + null, + false, + null, + true, + 'attr_code', + '', + 'sort_attr_code', + [ + 'attr_code' => [ + 'type' => 'text', + 'fields' => [ + 'sort_attr_code' => [ + 'type' => 'string', + 'index' => 'not_analyzed' + ] + ] + ], + 'store_id' => [ + 'type' => 'string', + 'index' => 'no' + ] + ] ] ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php index 9c717ea240a5d..6258a4a20d694 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php @@ -321,8 +321,15 @@ public function testGetAggregation() $this->clientMock->expects($this->once()) ->method('query') ->with($this->callback(function ($query) { + $histogramParams = $query['body']['aggregations']['prices']['histogram']; // Assert the interval is queried as a float. See MAGETWO-95471 - return $query['body']['aggregations']['prices']['histogram']['interval'] === 10.0; + if ($histogramParams['interval'] !== 10.0) { + return false; + } + if (!isset($histogramParams['min_doc_count']) || $histogramParams['min_doc_count'] !== 1) { + return false; + } + return true; })) ->willReturn([ 'aggregations' => [ diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php index 8114feb09d35d..d0ffc6debcd8a 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php @@ -5,14 +5,29 @@ */ namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface as TypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Match as MatchQueryBuilder; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; use Magento\Framework\Search\Request\Query\Match as MatchRequestQuery; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\MockObject\MockObject as MockObject; class MatchTest extends \PHPUnit\Framework\TestCase { + /** + * @var AttributeProvider|MockObject + */ + private $attributeProvider; + + /** + * @var TypeResolver|MockObject + */ + private $fieldTypeResolver; + /** * @var MatchQueryBuilder */ @@ -23,46 +38,63 @@ class MatchTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { + $this->attributeProvider = $this->createMock(AttributeProvider::class); + $this->fieldTypeResolver = $this->createMock(TypeResolver::class); + + $valueTransformerPoolMock = $this->createMock(ValueTransformerPool::class); + $valueTransformerMock = $this->createMock(ValueTransformerInterface::class); + $valueTransformerPoolMock->method('get') + ->willReturn($valueTransformerMock); + $valueTransformerMock->method('transform') + ->willReturnArgument(0); + $this->matchQueryBuilder = (new ObjectManager($this))->getObject( MatchQueryBuilder::class, [ 'fieldMapper' => $this->getFieldMapper(), 'preprocessorContainer' => [], + 'attributeProvider' => $this->attributeProvider, + 'fieldTypeResolver' => $this->fieldTypeResolver, + 'valueTransformerPool' => $valueTransformerPoolMock, ] ); } /** * Tests that method constructs a correct select query. - * @see MatchQueryBuilder::build - * - * @dataProvider queryValuesInvariantsProvider * - * @param string $rawQueryValue - * @param string $errorMessage + * @see MatchQueryBuilder::build */ - public function testBuild($rawQueryValue, $errorMessage) + public function testBuild() { - $this->assertSelectQuery( - $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'), - $errorMessage - ); - } + $attributeAdapter = $this->createMock(AttributeAdapter::class); + $this->attributeProvider->expects($this->once()) + ->method('getByAttributeCode') + ->with('some_field') + ->willReturn($attributeAdapter); + $this->fieldTypeResolver->expects($this->once()) + ->method('getFieldType') + ->with($attributeAdapter) + ->willReturn('text'); + + $rawQueryValue = 'query_value'; + $selectQuery = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'); - /** - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * - * @return array - */ - public function queryValuesInvariantsProvider() - { - return [ - ['query_value', 'Select query field must match simple raw query value.'], - ['query_value+', 'Specifying a trailing plus sign causes InnoDB to report a syntax error.'], - ['query_value-', 'Specifying a trailing minus sign causes InnoDB to report a syntax error.'], - ['query_@value', 'The @ symbol is reserved for use by the @distance proximity search operator.'], - ['query_value+@', 'The @ symbol is reserved for use by the @distance proximity search operator.'], + $expectedSelectQuery = [ + 'bool' => [ + 'must_not' => [ + [ + 'match' => [ + 'some_field' => [ + 'query' => $rawQueryValue, + 'boost' => 43, + ], + ], + ], + ], + ], ]; + $this->assertEquals($expectedSelectQuery, $selectQuery); } /** @@ -76,6 +108,16 @@ public function queryValuesInvariantsProvider() */ public function testBuildMatchQuery($rawQueryValue, $queryValue, $match) { + $attributeAdapter = $this->createMock(AttributeAdapter::class); + $this->attributeProvider->expects($this->once()) + ->method('getByAttributeCode') + ->with('some_field') + ->willReturn($attributeAdapter); + $this->fieldTypeResolver->expects($this->once()) + ->method('getFieldType') + ->with($attributeAdapter) + ->willReturn('text'); + $query = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'should'); $expectedSelectQuery = [ @@ -111,30 +153,6 @@ public function matchProvider() ]; } - /** - * @param array $selectQuery - * @param string $errorMessage - */ - private function assertSelectQuery($selectQuery, $errorMessage) - { - $expectedSelectQuery = [ - 'bool' => [ - 'must_not' => [ - [ - 'match' => [ - 'some_field' => [ - 'query' => 'query_value', - 'boost' => 43, - ], - ], - ], - ], - ], - ]; - - $this->assertEquals($expectedSelectQuery, $selectQuery, $errorMessage); - } - /** * Gets fieldMapper mock object. * diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php new file mode 100644 index 0000000000000..efd9073694129 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Framework\Search\RequestInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; + +/** + * Class SortTest + */ +class SortTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var Sort + */ + private $sortBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->attributeAdapterProvider = $this->getMockBuilder(AttributeProvider::class) + ->disableOriginalConstructor() + ->setMethods(['getByAttributeCode']) + ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); + + $this->sortBuilder = (new ObjectManager($this))->getObject( + Sort::class, + [ + 'attributeAdapterProvider' => $this->attributeAdapterProvider, + 'fieldNameResolver' => $this->fieldNameResolver, + ] + ); + } + + /** + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @dataProvider getSortProvider + * @param array $sortItems + * @param $isSortable + * @param $isFloatType + * @param $isIntegerType + * @param $fieldName + * @param array $expected + */ + public function testGetSort( + array $sortItems, + $isSortable, + $isFloatType, + $isIntegerType, + $fieldName, + array $expected + ) { + /** @var MockObject|RequestInterface $request */ + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getSort']) + ->getMockForAbstractClass(); + $request->expects($this->any()) + ->method('getSort') + ->willReturn($sortItems); + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isSortable', 'isFloatType', 'isIntegerType']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); + $attributeMock->expects($this->any()) + ->method('isFloatType') + ->willReturn($isFloatType); + $attributeMock->expects($this->any()) + ->method('isIntegerType') + ->willReturn($isIntegerType); + $this->attributeAdapterProvider->expects($this->any()) + ->method('getByAttributeCode') + ->with($this->anything()) + ->willReturn($attributeMock); + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->with($this->anything()) + ->will($this->returnCallback( + function ($attribute, $context) use ($fieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return 'sort_' . $fieldName; + } + } + )); + + $this->assertEquals( + $expected, + $this->sortBuilder->getSort($request) + ); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getSortProvider() + { + return [ + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ] + ], + null, + null, + null, + null, + [] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + 'price', + [ + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + true, + true, + true, + 'price', + [ + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'name', + 'direction' => 'DESC' + ], + ], + true, + false, + false, + 'name', + [ + [ + 'name.sort_name' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'not_eav_attribute', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + 'not_eav_attribute', + [ + [ + 'not_eav_attribute' => [ + 'order' => 'desc' + ] + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php index 9ea241b2fbf5c..d89e420457206 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/ResponseFactoryTest.php @@ -79,7 +79,7 @@ public function testCreate() 'itemTwo' => 45, ] ]; - $rawResponse = ['documents' => $documents, 'aggregations' => $aggregations]; + $rawResponse = ['documents' => $documents, 'aggregations' => $aggregations, 'total' => 2]; $exceptedResponse = [ 'documents' => [ @@ -102,6 +102,7 @@ public function testCreate() 'itemTwo' => 45 ], ], + 'total' => 2, ]; $this->documentFactory->expects($this->at(0))->method('create') @@ -118,7 +119,11 @@ public function testCreate() $this->objectManager->expects($this->once())->method('create') ->with( $this->equalTo(\Magento\Framework\Search\Response\QueryResponse::class), - $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => 'aggregationsData']) + $this->equalTo([ + 'documents' => ['document1', 'document2'], + 'aggregations' => 'aggregationsData', + 'total' => 2 + ]) ) ->will($this->returnValue('QueryResponseObject')); diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index a821506f5ef6e..c6ac38c1e4005 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~2.0|~5.1" + "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 6a42e4b3c9fe2..9732ae8226431 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -13,6 +13,124 @@ <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter" /> <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter" /> <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider" /> + <type name="Magento\Elasticsearch\Model\Config"> + <arguments> + <argument name="engineList" xsi:type="array"> + <item name="elasticsearch" xsi:type="string">elasticsearch</item> + <item name="elasticsearch5" xsi:type="string">elasticsearch5</item> + </argument> + </arguments> + </type> + + <virtualType name="Magento\Elasticsearch\Model\Layer\Search\Context" type="Magento\Catalog\Model\Layer\Search\Context"> + <arguments> + <argument name="collectionProvider" xsi:type="object">elasticsearchLayerSearchItemCollectionProvider</argument> + <argument name="stateKey" xsi:type="object">Magento\CatalogSearch\Model\Layer\Search\StateKey</argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Model\Layer\Search"> + <arguments> + <argument name="context" xsi:type="object">Magento\Elasticsearch\Model\Layer\Search\Context</argument> + </arguments> + </type> + <virtualType name="Magento\Elasticsearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> + <arguments> + <argument name="collectionProvider" xsi:type="object">elasticsearchLayerCategoryItemCollectionProvider</argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Model\Layer\Category"> + <arguments> + <argument name="context" xsi:type="object">Magento\Elasticsearch\Model\Layer\Category\Context</argument> + </arguments> + </type> + <virtualType name="elasticsearchFulltextSearchCollection" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">quick_search_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchFulltextSearchCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchFulltextSearchCollection</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</item> + <item name="elasticsearch" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchCategoryCollection" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">catalog_view_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchCategoryCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchCategoryCollection</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\CollectionFactory</item> + <item name="elasticsearch" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchAdvancedCollection" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">advanced_search_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchAdvancedCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchAdvancedCollection</argument> + </arguments> + </virtualType> + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="elasticsearch" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> + </arguments> + </type> + <virtualType name="elasticsearchSearchCriteriaResolverFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchSearchResultApplier\Factory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchTotalRecordsResolver\Factory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver</argument> + </arguments> + </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\FieldMapperResolver"> <arguments> <argument name="fieldMappers" xsi:type="array"> @@ -21,7 +139,7 @@ </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\DataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\DataMapper\DataMapperResolver" /> - <virtualType name="AdditionalFieldsForElasticsearchDataMapper" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> + <virtualType name="additionalFieldsProviderForElasticsearch" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> <arguments> <argument name="fieldsProviders" xsi:type="array"> <item name="categories" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy</item> @@ -31,7 +149,7 @@ </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper"> <arguments> - <argument name="additionalFieldsProvider" xsi:type="object">AdditionalFieldsForElasticsearchDataMapper</argument> + <argument name="additionalFieldsProvider" xsi:type="object">additionalFieldsProviderForElasticsearch</argument> </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\DataMapperResolver" /> @@ -68,7 +186,7 @@ </argument> </arguments> </type> - <type name="\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> <arguments> <argument name="productFieldMappers" xsi:type="array"> <item name="elasticsearch" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper</item> @@ -287,7 +405,7 @@ <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> </arguments> </type> - <type name="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> <arguments> <argument name="items" xsi:type="array"> <item name="notEav" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute</item> @@ -317,7 +435,7 @@ <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> - <type name="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> <arguments> <argument name="items" xsi:type="array"> <item name="integer" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType</item> @@ -327,7 +445,7 @@ </argument> </arguments> </type> - <type name="\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> <arguments> <argument name="items" xsi:type="array"> <item name="keyword" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType</item> @@ -360,6 +478,7 @@ <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> <argument name="fieldIndexResolver" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver</argument> <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> + <argument name="fieldNameResolver" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface</argument> </arguments> </virtualType> <virtualType name="elasticsearch5DynamicFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField"> @@ -368,12 +487,12 @@ <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> </arguments> </virtualType> - <type name="\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType"> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType"> <arguments> <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </type> - <type name="\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType"> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType"> <arguments> <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> @@ -393,13 +512,13 @@ <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> - <type name="\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> </arguments> </type> - <type name="\Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper"> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> <argument name="attributeAdapterProvider" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider</argument> <argument name="fieldProvider" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface</argument> @@ -417,7 +536,25 @@ <arguments> <argument name="pageSizeBySearchEngine" xsi:type="array"> <item name="elasticsearch" xsi:type="number">10000</item> - <item name="elasticsearch5" xsi:type="number">2147483647</item> + <item name="elasticsearch5" xsi:type="number">10000</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool"> + <arguments> + <argument name="valueTransformers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer</item> + <item name="date" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\DateTransformer</item> + <item name="float" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\FloatTransformer</item> + <item name="integer" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\IntegerTransformer</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer"> + <arguments> + <argument name="preprocessors" xsi:type="array"> + <item name="stopwordsPreprocessor" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\Preprocessor\Stopwords</item> + <item name="synonymsPreprocessor" xsi:type="object">Magento\Search\Adapter\Query\Preprocessor\Synonyms</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php new file mode 100644 index 0000000000000..1b17db1a00f6e --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Block\Adminhtml\System\Config; + +/** + * Elasticsearch 6.x test connection block + */ +class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection +{ + /** + * @inheritdoc + */ + protected function _getFieldMapping() + { + $fields = [ + 'engine' => 'catalog_search_engine', + 'hostname' => 'catalog_search_elasticsearch6_server_hostname', + 'port' => 'catalog_search_elasticsearch6_server_port', + 'index' => 'catalog_search_elasticsearch6_index_prefix', + 'enableAuth' => 'catalog_search_elasticsearch6_enable_auth', + 'username' => 'catalog_search_elasticsearch6_username', + 'password' => 'catalog_search_elasticsearch6_password', + 'timeout' => 'catalog_search_elasticsearch6_server_timeout', + ]; + + return array_merge(parent::_getFieldMapping(), $fields); + } +} diff --git a/app/code/Magento/Elasticsearch6/LICENSE.txt b/app/code/Magento/Elasticsearch6/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/Elasticsearch6/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch6/LICENSE_AFL.txt b/app/code/Magento/Elasticsearch6/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php new file mode 100644 index 0000000000000..7532927f1dc85 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch6\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver as Base; + +/** + * Default name resolver. + */ +class DefaultResolver extends Base +{ + /** + * Get field name. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + $fieldName = parent::getFieldName($attribute, $context); + + if ($fieldName === '_all') { + $fieldName = '_search'; + } + + return $fieldName; + } +} diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php new file mode 100644 index 0000000000000..af39b24acda56 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -0,0 +1,332 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Model\Client; + +use Magento\Framework\Exception\LocalizedException; +use Magento\AdvancedSearch\Model\Client\ClientInterface; + +/** + * Elasticsearch client + */ +class Elasticsearch implements ClientInterface +{ + /** + * Elasticsearch Client instances + * + * @var \Elasticsearch\Client[] + */ + private $client; + + /** + * @var array + */ + private $clientOptions; + + /** + * @var bool + */ + private $pingResult; + + /** + * Initialize Elasticsearch Client + * + * @param array $options + * @param \Elasticsearch\Client|null $elasticsearchClient + * @throws LocalizedException + */ + public function __construct( + $options = [], + $elasticsearchClient = null + ) { + if (empty($options['hostname']) || ((!empty($options['enableAuth']) && + ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + throw new LocalizedException( + __('The search failed because of a search engine misconfiguration.') + ); + } + + if (!($elasticsearchClient instanceof \Elasticsearch\Client)) { + $config = $this->buildConfig($options); + $elasticsearchClient = \Elasticsearch\ClientBuilder::fromConfig($config, true); + } + $this->client[getmypid()] = $elasticsearchClient; + $this->clientOptions = $options; + } + + /** + * Get Elasticsearch Client + * + * @return \Elasticsearch\Client + */ + private function getClient() + { + $pid = getmypid(); + if (!isset($this->client[$pid])) { + $config = $this->buildConfig($this->clientOptions); + $this->client[$pid] = \Elasticsearch\ClientBuilder::fromConfig($config, true); + } + return $this->client[$pid]; + } + + /** + * Ping the Elasticsearch client + * + * @return bool + */ + public function ping() + { + if ($this->pingResult === null) { + $this->pingResult = $this->getClient()->ping(['client' => ['timeout' => $this->clientOptions['timeout']]]); + } + + return $this->pingResult; + } + + /** + * Validate connection params + * + * @return bool + */ + public function testConnection() + { + return $this->ping(); + } + + /** + * Build config. + * + * @param array $options + * @return array + */ + private function buildConfig($options = []) + { + $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); + if (!$protocol) { + $protocol = 'http'; + } + if (!empty($options['port'])) { + $host .= ':' . $options['port']; + } + if (!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) { + $host = sprintf('%s://%s:%s@%s', $protocol, $options['username'], $options['password'], $host); + } + + $options['hosts'] = [$host]; + return $options; + } + + /** + * Performs bulk query over Elasticsearch index + * + * @param array $query + * @return void + */ + public function bulkQuery($query) + { + $this->getClient()->bulk($query); + } + + /** + * Creates an Elasticsearch index. + * + * @param string $index + * @param array $settings + * @return void + */ + public function createIndex($index, $settings) + { + $this->getClient()->indices()->create([ + 'index' => $index, + 'body' => $settings, + ]); + } + + /** + * Delete an Elasticsearch index. + * + * @param string $index + * @return void + */ + public function deleteIndex($index) + { + $this->getClient()->indices()->delete(['index' => $index]); + } + + /** + * Check if index is empty. + * + * @param string $index + * @return bool + */ + public function isEmptyIndex($index) + { + $stats = $this->getClient()->indices()->stats(['index' => $index, 'metric' => 'docs']); + if ($stats['indices'][$index]['primaries']['docs']['count'] == 0) { + return true; + } + return false; + } + + /** + * Updates alias. + * + * @param string $alias + * @param string $newIndex + * @param string $oldIndex + * @return void + */ + public function updateAlias($alias, $newIndex, $oldIndex = '') + { + $params['body'] = ['actions' => []]; + if ($oldIndex) { + $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; + } + if ($newIndex) { + $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; + } + + $this->getClient()->indices()->updateAliases($params); + } + + /** + * Checks whether Elasticsearch index exists + * + * @param string $index + * @return bool + */ + public function indexExists($index) + { + return $this->getClient()->indices()->exists(['index' => $index]); + } + + /** + * Exists alias. + * + * @param string $alias + * @param string $index + * @return bool + */ + public function existsAlias($alias, $index = '') + { + $params = ['name' => $alias]; + if ($index) { + $params['index'] = $index; + } + return $this->getClient()->indices()->existsAlias($params); + } + + /** + * Get alias. + * + * @param string $alias + * @return array + */ + public function getAlias($alias) + { + return $this->getClient()->indices()->getAlias(['name' => $alias]); + } + + /** + * Add mapping to Elasticsearch index + * + * @param array $fields + * @param string $index + * @param string $entityType + * @return void + */ + public function addFieldsMapping(array $fields, $index, $entityType) + { + $params = [ + 'index' => $index, + 'type' => $entityType, + 'body' => [ + $entityType => [ + 'properties' => [ + '_search' => [ + 'type' => 'text' + ], + ], + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], + ], + ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' + ], + ], + ], + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'int', + ], + ], + ], + ], + ], + ], + ]; + + foreach ($fields as $field => $fieldInfo) { + $params['body'][$entityType]['properties'][$field] = $fieldInfo; + } + + $this->getClient()->indices()->putMapping($params); + } + + /** + * Delete mapping in Elasticsearch index + * + * @param string $index + * @param string $entityType + * @return void + */ + public function deleteMapping($index, $entityType) + { + $this->getClient()->indices()->deleteMapping([ + 'index' => $index, + 'type' => $entityType, + ]); + } + + /** + * Execute search by $query + * + * @param array $query + * @return array + */ + public function query($query) + { + return $this->getClient()->search($query); + } + + /** + * Execute suggest query + * + * @param array $query + * @return array + */ + public function suggest($query) + { + return $this->getClient()->suggest($query); + } +} diff --git a/app/code/Magento/Elasticsearch6/Model/DataProvider/Suggestions.php b/app/code/Magento/Elasticsearch6/Model/DataProvider/Suggestions.php new file mode 100644 index 0000000000000..d05471734bb8f --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Model/DataProvider/Suggestions.php @@ -0,0 +1,275 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Model\DataProvider; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Search\Model\QueryInterface; +use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Search\Model\QueryResultFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Store\Model\StoreManagerInterface as StoreManager; + +/** + * Class Suggestions + */ +class Suggestions implements SuggestedQueriesInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var QueryResultFactory + */ + private $queryResultFactory; + + /** + * @var ConnectionManager + */ + private $connectionManager; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var SearchIndexNameResolver + */ + private $searchIndexNameResolver; + + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @var FieldProviderInterface + */ + private $fieldProvider; + + /** + * Suggestions constructor. + * + * @param ScopeConfigInterface $scopeConfig + * @param Config $config + * @param QueryResultFactory $queryResultFactory + * @param ConnectionManager $connectionManager + * @param SearchIndexNameResolver $searchIndexNameResolver + * @param StoreManager $storeManager + * @param FieldProviderInterface $fieldProvider + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Config $config, + QueryResultFactory $queryResultFactory, + ConnectionManager $connectionManager, + SearchIndexNameResolver $searchIndexNameResolver, + StoreManager $storeManager, + FieldProviderInterface $fieldProvider + ) { + $this->queryResultFactory = $queryResultFactory; + $this->connectionManager = $connectionManager; + $this->scopeConfig = $scopeConfig; + $this->config = $config; + $this->searchIndexNameResolver = $searchIndexNameResolver; + $this->storeManager = $storeManager; + $this->fieldProvider = $fieldProvider; + } + + /** + * @inheritdoc + */ + public function getItems(QueryInterface $query) + { + $result = []; + if ($this->isSuggestionsAllowed()) { + $isResultsCountEnabled = $this->isResultsCountEnabled(); + + foreach ($this->getSuggestions($query) as $suggestion) { + $count = null; + if ($isResultsCountEnabled) { + $count = isset($suggestion['freq']) ? $suggestion['freq'] : null; + } + $result[] = $this->queryResultFactory->create( + [ + 'queryText' => $suggestion['text'], + 'resultsCount' => $count, + ] + ); + } + } + + return $result; + } + + /** + * @inheritdoc + */ + public function isResultsCountEnabled() + { + return $this->scopeConfig->isSetFlag( + SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT_RESULTS_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Get Suggestions + * + * @param QueryInterface $query + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getSuggestions(QueryInterface $query) + { + $suggestions = []; + $searchSuggestionsCount = $this->getSearchSuggestionsCount(); + + $searchQuery = $this->initQuery($query); + $searchQuery = $this->addSuggestFields($searchQuery, $searchSuggestionsCount); + + $result = $this->fetchQuery($searchQuery); + + if (is_array($result)) { + foreach ($result['suggest'] ?? [] as $suggest) { + foreach ($suggest as $token) { + foreach ($token['options'] ?? [] as $key => $suggestion) { + $suggestions[$suggestion['score'] . '_' . $key] = $suggestion; + } + } + } + ksort($suggestions); + $texts = array_unique(array_column($suggestions, 'text')); + $suggestions = array_slice( + array_intersect_key(array_values($suggestions), $texts), + 0, + $searchSuggestionsCount + ); + } + + return $suggestions; + } + + /** + * Init Search Query + * + * @param string $query + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function initQuery($query) + { + $searchQuery = [ + 'index' => $this->searchIndexNameResolver->getIndexName( + $this->storeManager->getStore()->getId(), + Config::ELASTICSEARCH_TYPE_DEFAULT + ), + 'type' => Config::ELASTICSEARCH_TYPE_DEFAULT, + 'body' => [ + 'suggest' => [ + 'text' => $query->getQueryText() + ] + ], + ]; + + return $searchQuery; + } + + /** + * Build Suggest on searchable fields. + * + * @param array $searchQuery + * @param int $searchSuggestionsCount + * + * @return array + */ + private function addSuggestFields($searchQuery, $searchSuggestionsCount) + { + $fields = $this->getSuggestFields(); + foreach ($fields as $field) { + $searchQuery['body']['suggest']['phrase_' . $field] = [ + 'phrase' => [ + 'field' => $field, + 'analyzer' => 'standard', + 'size' => $searchSuggestionsCount, + 'max_errors' => 1, + 'direct_generator' => [ + [ + 'field' => $field, + 'min_word_length' => 3, + 'min_doc_freq' => 1, + ] + ], + ], + ]; + } + + return $searchQuery; + } + + /** + * Get fields to build suggest query on. + * + * @return array + */ + private function getSuggestFields() + { + $fields = array_filter($this->fieldProvider->getFields(), function ($field) { + return (($field['type'] ?? null) === 'text') && (($field['index'] ?? null) !== false); + }); + + return array_keys($fields); + } + + /** + * Fetch Query + * + * @param array $query + * @return array + */ + private function fetchQuery(array $query) + { + return $this->connectionManager->getConnection()->query($query); + } + + /** + * Get search suggestions Max Count from config + * + * @return int + */ + private function getSearchSuggestionsCount() + { + return (int) $this->scopeConfig->getValue( + SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is Search Suggestions Allowed + * + * @return bool + */ + private function isSuggestionsAllowed() + { + $isSuggestionsEnabled = $this->scopeConfig->isSetFlag( + SuggestedQueriesInterface::SEARCH_SUGGESTION_ENABLED, + ScopeInterface::SCOPE_STORE + ); + $isEnabled = $this->config->isElasticsearchEnabled(); + $isSuggestionsAllowed = ($isEnabled && $isSuggestionsEnabled); + + return $isSuggestionsAllowed; + } +} diff --git a/app/code/Magento/Elasticsearch6/README.md b/app/code/Magento/Elasticsearch6/README.md new file mode 100644 index 0000000000000..8bf95ad95d147 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/README.md @@ -0,0 +1,2 @@ +Magento\Elasticsearch module allows to use Elastic search engine (v6) for product searching capabilities. +The module implements Magento\Search library interfaces. diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php new file mode 100644 index 0000000000000..a3c6e7e148f3d --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch6\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface + as FieldTypeResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch6\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver; + +/** + * @SuppressWarnings(PHPMD) + */ +class DefaultResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DefaultResolver + */ + private $resolver; + + /** + * @var FieldTypeResolver + */ + private $fieldTypeResolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $objectManager = new ObjectManagerHelper($this); + $this->fieldTypeResolver = $this->getMockBuilder(FieldTypeResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldType']) + ->getMockForAbstractClass(); + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $this->resolver = $objectManager->getObject( + DefaultResolver::class, + [ + 'fieldTypeResolver' => $this->fieldTypeResolver, + 'fieldTypeConverter' => $this->fieldTypeConverter + ] + ); + } + + /** + * @dataProvider getFieldNameProvider + * @param $fieldType + * @param $attributeCode + * @param $frontendInput + * @param $isSortable + * @param $context + * @param $expected + * @return void + */ + public function testGetFieldName( + $fieldType, + $attributeCode, + $frontendInput, + $isSortable, + $context, + $expected + ) { + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('string'); + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $attributeMock->expects($this->any()) + ->method('getFrontendInput') + ->willReturn($frontendInput); + $attributeMock->expects($this->any()) + ->method('isSortable') + ->willReturn($isSortable); + $this->fieldTypeResolver->expects($this->any()) + ->method('getFieldType') + ->willReturn($fieldType); + + $this->assertEquals( + $expected, + $this->resolver->getFieldName($attributeMock, $context) + ); + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['', 'code', '', false, [], 'code'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['string', '*', '', false, ['type' => 'default'], '_search'], + ['', 'code', '', false, ['type' => 'default'], 'code'], + ['', 'code', 'select', false, ['type' => 'default'], 'code'], + ['', 'code', 'boolean', false, ['type' => 'default'], 'code'], + ['', 'code', '', true, ['type' => 'sort'], 'sort_code'], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php new file mode 100644 index 0000000000000..8276d0dd8dbe8 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -0,0 +1,562 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Test\Unit\Model\Client; + +use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +class ElasticsearchTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ElasticsearchClient + */ + protected $model; + + /** + * @var \Elasticsearch\Client|\PHPUnit_Framework_MockObject_MockObject + */ + protected $elasticsearchClientMock; + + /** + * @var \Elasticsearch\Namespaces\IndicesNamespace|\PHPUnit_Framework_MockObject_MockObject + */ + protected $indicesMock; + + /** + * @var ObjectManagerHelper + */ + protected $objectManager; + + /** + * Setup + * + * @return void + */ + protected function setUp() + { + $this->elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class) + ->setMethods([ + 'indices', + 'ping', + 'bulk', + 'search', + 'scroll', + 'suggest', + 'info', + ]) + ->disableOriginalConstructor() + ->getMock(); + $this->indicesMock = $this->getMockBuilder(\Elasticsearch\Namespaces\IndicesNamespace::class) + ->setMethods([ + 'exists', + 'getSettings', + 'create', + 'delete', + 'putMapping', + 'deleteMapping', + 'stats', + 'updateAliases', + 'existsAlias', + 'getAlias', + ]) + ->disableOriginalConstructor() + ->getMock(); + $this->elasticsearchClientMock->expects($this->any()) + ->method('indices') + ->willReturn($this->indicesMock); + $this->elasticsearchClientMock->expects($this->any()) + ->method('ping') + ->willReturn(true); + $this->elasticsearchClientMock->expects($this->any()) + ->method('info') + ->willReturn(['version' => ['number' => '6.0.0']]); + + $this->objectManager = new ObjectManagerHelper($this); + $this->model = $this->objectManager->getObject( + \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + [ + 'options' => $this->getOptions(), + 'elasticsearchClient' => $this->elasticsearchClientMock + ] + ); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testConstructorOptionsException() + { + $result = $this->objectManager->getObject( + \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + [ + 'options' => [] + ] + ); + $this->assertNotNull($result); + } + + /** + * Test client creation from the list of options + */ + public function testConstructorWithOptions() + { + $result = $this->objectManager->getObject( + \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + [ + 'options' => $this->getOptions() + ] + ); + $this->assertNotNull($result); + } + + /** + * Test ping functionality + */ + public function testPing() + { + $this->elasticsearchClientMock->expects($this->once())->method('ping')->willReturn(true); + $this->assertEquals(true, $this->model->ping()); + } + + /** + * Test validation of connection parameters + */ + public function testTestConnection() + { + $this->elasticsearchClientMock->expects($this->once())->method('ping')->willReturn(true); + $this->assertEquals(true, $this->model->testConnection()); + } + + /** + * Test validation of connection parameters returns false + */ + public function testTestConnectionFalse() + { + $this->elasticsearchClientMock->expects($this->once())->method('ping')->willReturn(false); + $this->assertEquals(true, $this->model->testConnection()); + } + + /** + * Test validation of connection parameters + */ + public function testTestConnectionPing() + { + $this->model = $this->objectManager->getObject( + \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + [ + 'options' => $this->getEmptyIndexOption(), + 'elasticsearchClient' => $this->elasticsearchClientMock + ] + ); + + $this->model->ping(); + $this->assertEquals(true, $this->model->testConnection()); + } + + /** + * Test bulkQuery() method + */ + public function testBulkQuery() + { + $this->elasticsearchClientMock->expects($this->once()) + ->method('bulk') + ->with([]); + $this->model->bulkQuery([]); + } + + /** + * Test createIndex() method, case when such index exists + */ + public function testCreateIndexExists() + { + $this->indicesMock->expects($this->once()) + ->method('create') + ->with([ + 'index' => 'indexName', + 'body' => [], + ]); + $this->model->createIndex('indexName', []); + } + + /** + * Test deleteIndex() method. + */ + public function testDeleteIndex() + { + $this->indicesMock->expects($this->once()) + ->method('delete') + ->with(['index' => 'indexName']); + $this->model->deleteIndex('indexName'); + } + + /** + * Test isEmptyIndex() method. + */ + public function testIsEmptyIndex() + { + $indexName = 'magento2_index'; + $stats['indices'][$indexName]['primaries']['docs']['count'] = 0; + + $this->indicesMock->expects($this->once()) + ->method('stats') + ->with(['index' => $indexName, 'metric' => 'docs']) + ->willReturn($stats); + $this->assertTrue($this->model->isEmptyIndex($indexName)); + } + + /** + * Test isEmptyIndex() method returns false. + */ + public function testIsEmptyIndexFalse() + { + $indexName = 'magento2_index'; + $stats['indices'][$indexName]['primaries']['docs']['count'] = 1; + + $this->indicesMock->expects($this->once()) + ->method('stats') + ->with(['index' => $indexName, 'metric' => 'docs']) + ->willReturn($stats); + $this->assertFalse($this->model->isEmptyIndex($indexName)); + } + + /** + * Test updateAlias() method with new index. + */ + public function testUpdateAlias() + { + $alias = 'alias1'; + $index = 'index1'; + + $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $index]]; + + $this->indicesMock->expects($this->once()) + ->method('updateAliases') + ->with($params); + $this->model->updateAlias($alias, $index); + } + + /** + * Test updateAlias() method with new and old index. + */ + public function testUpdateAliasRemoveOldIndex() + { + $alias = 'alias1'; + $newIndex = 'index1'; + $oldIndex = 'indexOld'; + + $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; + $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; + + $this->indicesMock->expects($this->once()) + ->method('updateAliases') + ->with($params); + $this->model->updateAlias($alias, $newIndex, $oldIndex); + } + + /** + * Test indexExists() method, case when no such index exists + */ + public function testIndexExists() + { + $this->indicesMock->expects($this->once()) + ->method('exists') + ->with([ + 'index' => 'indexName', + ]) + ->willReturn(true); + $this->model->indexExists('indexName'); + } + + /** + * Tests existsAlias() method checking for alias. + */ + public function testExistsAlias() + { + $alias = 'alias1'; + $params = ['name' => $alias]; + $this->indicesMock->expects($this->once()) + ->method('existsAlias') + ->with($params) + ->willReturn(true); + $this->assertTrue($this->model->existsAlias($alias)); + } + + /** + * Tests existsAlias() method checking for alias and index. + */ + public function testExistsAliasWithIndex() + { + $alias = 'alias1'; + $index = 'index1'; + $params = ['name' => $alias, 'index' => $index]; + $this->indicesMock->expects($this->once()) + ->method('existsAlias') + ->with($params) + ->willReturn(true); + $this->assertTrue($this->model->existsAlias($alias, $index)); + } + + /** + * Test getAlias() method. + */ + public function testGetAlias() + { + $alias = 'alias1'; + $params = ['name' => $alias]; + $this->indicesMock->expects($this->once()) + ->method('getAlias') + ->with($params) + ->willReturn([]); + $this->assertEquals([], $this->model->getAlias($alias)); + } + + /** + * Test createIndexIfNotExists() method, case when operation fails + * @expectedException \Exception + */ + public function testCreateIndexFailure() + { + $this->indicesMock->expects($this->once()) + ->method('create') + ->with([ + 'index' => 'indexName', + 'body' => [], + ]) + ->willThrowException(new \Exception('Something went wrong')); + $this->model->createIndex('indexName', []); + } + + /** + * Test testAddFieldsMapping() method + */ + public function testAddFieldsMapping() + { + $this->indicesMock->expects($this->once()) + ->method('putMapping') + ->with([ + 'index' => 'indexName', + 'type' => 'product', + 'body' => [ + 'product' => [ + 'properties' => [ + '_search' => [ + 'type' => 'text', + ], + 'name' => [ + 'type' => 'text', + ], + ], + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], + ], + ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' + ], + ], + ], + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'int', + ], + ], + ], + ], + ], + ], + ]); + $this->model->addFieldsMapping( + [ + 'name' => [ + 'type' => 'text', + ], + ], + 'indexName', + 'product' + ); + } + + /** + * Test testAddFieldsMapping() method + * @expectedException \Exception + */ + public function testAddFieldsMappingFailure() + { + $this->indicesMock->expects($this->once()) + ->method('putMapping') + ->with([ + 'index' => 'indexName', + 'type' => 'product', + 'body' => [ + 'product' => [ + 'properties' => [ + '_search' => [ + 'type' => 'text', + ], + 'name' => [ + 'type' => 'text', + ], + ], + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], + ], + ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' + ], + ], + ], + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'int', + ], + ], + ], + ], + ], + ], + ]) + ->willThrowException(new \Exception('Something went wrong')); + $this->model->addFieldsMapping( + [ + 'name' => [ + 'type' => 'text', + ], + ], + 'indexName', + 'product' + ); + } + + /** + * Test deleteMapping() method + */ + public function testDeleteMapping() + { + $this->indicesMock->expects($this->once()) + ->method('deleteMapping') + ->with([ + 'index' => 'indexName', + 'type' => 'product', + ]); + $this->model->deleteMapping( + 'indexName', + 'product' + ); + } + + /** + * Test deleteMapping() method + * @expectedException \Exception + */ + public function testDeleteMappingFailure() + { + $this->indicesMock->expects($this->once()) + ->method('deleteMapping') + ->with([ + 'index' => 'indexName', + 'type' => 'product', + ]) + ->willThrowException(new \Exception('Something went wrong')); + $this->model->deleteMapping( + 'indexName', + 'product' + ); + } + + /** + * Test query() method + * @return void + */ + public function testQuery() + { + $query = 'test phrase query'; + $this->elasticsearchClientMock->expects($this->once()) + ->method('search') + ->with($query) + ->willReturn([]); + $this->assertEquals([], $this->model->query($query)); + } + + /** + * Test suggest() method + * @return void + */ + public function testSuggest() + { + $query = 'query'; + $this->elasticsearchClientMock->expects($this->once()) + ->method('suggest') + ->willReturn([]); + $this->assertEquals([], $this->model->suggest($query)); + } + + /** + * Get elasticsearch client options + * + * @return array + */ + protected function getOptions() + { + return [ + 'hostname' => 'localhost', + 'port' => '9200', + 'timeout' => 15, + 'index' => 'magento2', + 'enableAuth' => 1, + 'username' => 'user', + 'password' => 'passwd', + ]; + } + + /** + * @return array + */ + protected function getEmptyIndexOption() + { + return [ + 'hostname' => 'localhost', + 'port' => '9200', + 'index' => '', + 'timeout' => 15, + 'enableAuth' => 1, + 'username' => 'user', + 'password' => 'passwd', + ]; + } +} diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/DataProvider/SuggestionsTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/DataProvider/SuggestionsTest.php new file mode 100644 index 0000000000000..b3c60b70ffa8e --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/DataProvider/SuggestionsTest.php @@ -0,0 +1,183 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Test\Unit\Model\DataProvider; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Model\DataProvider\Suggestions; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Search\Model\QueryResultFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Search\Model\QueryInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SuggestionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Suggestions + */ + private $model; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $config; + + /** + * @var QueryResultFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryResultFactory; + + /** + * @var ConnectionManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionManager; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var SearchIndexNameResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchIndexNameResolver; + + /** + * @var StoreManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var QueryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $query; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->config = $this->getMockBuilder(\Magento\Elasticsearch\Model\Config::class) + ->disableOriginalConstructor() + ->setMethods(['isElasticsearchEnabled']) + ->getMock(); + + $this->queryResultFactory = $this->getMockBuilder(\Magento\Search\Model\QueryResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->connectionManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class) + ->disableOriginalConstructor() + ->setMethods(['getConnection']) + ->getMock(); + + $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->searchIndexNameResolver = $this + ->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getIndexName']) + ->getMock(); + + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->query = $this->getMockBuilder(\Magento\Search\Model\QueryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->model = $objectManager->getObject( + \Magento\Elasticsearch6\Model\DataProvider\Suggestions::class, + [ + 'queryResultFactory' => $this->queryResultFactory, + 'connectionManager' => $this->connectionManager, + 'scopeConfig' => $this->scopeConfig, + 'config' => $this->config, + 'searchIndexNameResolver' => $this->searchIndexNameResolver, + 'storeManager' => $this->storeManager + ] + ); + } + + /** + * Test getItems() method + */ + public function testGetItems() + { + $this->scopeConfig->expects($this->any()) + ->method('getValue') + ->willReturn(1); + + $this->config->expects($this->any()) + ->method('isElasticsearchEnabled') + ->willReturn(1); + + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($store); + + $store->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $this->searchIndexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('magento2_product_1'); + + $this->query->expects($this->any()) + ->method('getQueryText') + ->willReturn('query'); + + $client = $this->getMockBuilder(\Magento\Elasticsearch6\Model\Client\Elasticsearch::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionManager->expects($this->any()) + ->method('getConnection') + ->willReturn($client); + + $client->expects($this->any()) + ->method('query') + ->willReturn([ + 'suggest' => [ + 'phrase_field' => [ + 'options' => [ + 'text' => 'query', + 'score' => 1, + 'freq' => 1, + ] + ], + ], + ]); + + $query = $this->getMockBuilder(\Magento\Search\Model\QueryResult::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->queryResultFactory->expects($this->any()) + ->method('create') + ->willReturn($query); + + $this->assertInternalType('array', $this->model->getItems($this->query)); + } +} diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json new file mode 100644 index 0000000000000..26b6c8c678ade --- /dev/null +++ b/app/code/Magento/Elasticsearch6/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-elasticsearch-6", + "description": "N/A", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-advanced-search": "*", + "magento/module-catalog-search": "*", + "magento/module-search": "*", + "magento/module-store": "*", + "magento/module-elasticsearch": "*", + "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1" + }, + "suggest": { + "magento/module-config": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Elasticsearch6\\": "" + } + } +} diff --git a/app/code/Magento/Elasticsearch6/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch6/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..067a0acb8c908 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/etc/adminhtml/system.xml @@ -0,0 +1,85 @@ +<?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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="catalog"> + <group id="search"> + <!-- Elasticsearch 6.0+ --> + <field id="elasticsearch6_server_hostname" translate="label" type="text" sortOrder="71" + showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Elasticsearch Server Hostname</label> + <depends> + <field id="engine">elasticsearch6</field> + </depends> + </field> + + <field id="elasticsearch6_server_port" translate="label" type="text" sortOrder="72" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label>Elasticsearch Server Port</label> + <depends> + <field id="engine">elasticsearch6</field> + </depends> + </field> + + <field id="elasticsearch6_index_prefix" translate="label" type="text" sortOrder="73" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label>Elasticsearch Index Prefix</label> + <depends> + <field id="engine">elasticsearch6</field> + </depends> + </field> + + <field id="elasticsearch6_enable_auth" translate="label" type="select" sortOrder="74" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label>Enable Elasticsearch HTTP Auth</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="engine">elasticsearch6</field> + </depends> + </field> + + <field id="elasticsearch6_username" translate="label" type="text" sortOrder="75" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label>Elasticsearch HTTP Username</label> + <depends> + <field id="engine">elasticsearch6</field> + <field id="elasticsearch6_enable_auth">1</field> + </depends> + </field> + + <field id="elasticsearch6_password" translate="label" type="text" sortOrder="76" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label>Elasticsearch HTTP Password</label> + <depends> + <field id="engine">elasticsearch6</field> + <field id="elasticsearch6_enable_auth">1</field> + </depends> + </field> + + <field id="elasticsearch6_server_timeout" translate="label" type="text" sortOrder="77" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label>Elasticsearch Server Timeout</label> + <depends> + <field id="engine">elasticsearch6</field> + </depends> + </field> + + <field id="elasticsearch6_test_connect_wizard" translate="button_label" sortOrder="78" showInDefault="1" + showInWebsite="0" showInStore="0"> + <label/> + <button_label>Test Connection</button_label> + <frontend_model>Magento\Elasticsearch6\Block\Adminhtml\System\Config\TestConnection</frontend_model> + <depends> + <field id="engine">elasticsearch6</field> + </depends> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/Elasticsearch6/etc/config.xml b/app/code/Magento/Elasticsearch6/etc/config.xml new file mode 100644 index 0000000000000..047ae977fdef1 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/etc/config.xml @@ -0,0 +1,20 @@ +<?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:module:Magento_Store:etc/config.xsd"> + <default> + <catalog> + <search> + <elasticsearch6_server_hostname>localhost</elasticsearch6_server_hostname> + <elasticsearch6_server_port>9200</elasticsearch6_server_port> + <elasticsearch6_index_prefix>magento2</elasticsearch6_index_prefix> + <elasticsearch6_enable_auth>0</elasticsearch6_enable_auth> + <elasticsearch6_server_timeout>15</elasticsearch6_server_timeout> + </search> + </catalog> + </default> +</config> diff --git a/app/code/Magento/Elasticsearch6/etc/di.xml b/app/code/Magento/Elasticsearch6/etc/di.xml new file mode 100644 index 0000000000000..011dfa1019738 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -0,0 +1,205 @@ +<?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\Elasticsearch\Model\Config"> + <arguments> + <argument name="engineList" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">elasticsearch6</item> + </argument> + </arguments> + </type> + + <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> + <arguments> + <argument name="engines" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Elasticsearch 6.0+</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> + <arguments> + <argument name="categoryFieldsProviders" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapperProxy"> + <arguments> + <argument name="dataMappers" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapper</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> + <arguments> + <argument name="productFieldMappers" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch6\Model\Adapter\FieldMapper\ProductFieldMapper</item> + </argument> + </arguments> + </type> + + <type name="Magento\AdvancedSearch\Model\Client\ClientResolver"> + <arguments> + <argument name="clientFactories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">\Magento\Elasticsearch6\Model\Client\ElasticsearchFactory</item> + </argument> + <argument name="clientOptions" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">\Magento\Elasticsearch\Model\Config</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexerHandler</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Indexer\IndexStructureFactory"> + <arguments> + <argument name="structures" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexStructure</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\ResourceModel\EngineProvider"> + <arguments> + <argument name="engines" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Engine</item> + </argument> + </arguments> + </type> + + <type name="Magento\Search\Model\AdapterFactory"> + <arguments> + <argument name="adapters" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter</item> + </argument> + </arguments> + </type> + + <type name="Magento\Search\Model\EngineResolver"> + <arguments> + <argument name="engines" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">elasticsearch6</item> + </argument> + </arguments> + </type> + + <virtualType name="Magento\Elasticsearch6\Model\Client\ElasticsearchFactory" type="Magento\AdvancedSearch\Model\Client\ClientFactory"> + <arguments> + <argument name="clientClass" xsi:type="string">Magento\Elasticsearch6\Model\Client\Elasticsearch</argument> + </arguments> + </virtualType> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy"> + <arguments> + <argument name="clientFactories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch6\Model\Client\ElasticsearchFactory</item> + </argument> + </arguments> + </type> + + <type name="Magento\Framework\Search\Dynamic\IntervalFactory"> + <arguments> + <argument name="intervals" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval</item> + </argument> + </arguments> + </type> + + <type name="Magento\Framework\Search\Dynamic\DataProviderFactory"> + <arguments> + <argument name="dataProviders" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider</item> + </argument> + </arguments> + </type> + + + <type name="Magento\AdvancedSearch\Model\SuggestedQueries"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch6\Model\DataProvider\Suggestions</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch6\Model\DataProvider\Suggestions"> + <arguments> + <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + </arguments> + </type> + + <virtualType name="elasticsearch6FieldNameResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> + <arguments> + <argument name="items" xsi:type="array"> + <item name="notEav" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute</item> + <item name="special" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\SpecialAttribute</item> + <item name="price" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price</item> + <item name="categoryName" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName</item> + <item name="position" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position</item> + <item name="default" xsi:type="object">\Magento\Elasticsearch6\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver</item> + </argument> + </arguments> + </virtualType> + + <virtualType name="Magento\Elasticsearch6\Model\Adapter\FieldMapper\ProductFieldMapper" + type="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + <arguments> + <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearch6FieldNameResolver</argument> + </arguments> + </virtualType> + + <type name="Magento\Search\Model\Search\PageSizeProvider"> + <arguments> + <argument name="pageSizeBySearchEngine" xsi:type="array"> + <item name="elasticsearch6" xsi:type="number">10000</item> + </argument> + </arguments> + </type> + + <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + </argument> + </arguments> + </virtualType> + + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> + </arguments> + </type> + + <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/Elasticsearch6/etc/module.xml b/app/code/Magento/Elasticsearch6/etc/module.xml new file mode 100644 index 0000000000000..4fde2394dfbdd --- /dev/null +++ b/app/code/Magento/Elasticsearch6/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_Elasticsearch6"> + <sequence> + <module name="Magento_CatalogSearch"/> + <module name="Magento_Search"/> + <module name="Magento_AdvancedSearch"/> + <module name="Magento_Store"/> + <module name="Magento_Elasticsearch"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/Elasticsearch6/registration.php b/app/code/Magento/Elasticsearch6/registration.php new file mode 100644 index 0000000000000..7ab10e996eb8c --- /dev/null +++ b/app/code/Magento/Elasticsearch6/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_Elasticsearch6', + __DIR__ +); diff --git a/app/code/Magento/Email/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Email/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..5086d74efa606 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Data/AdminMenuData.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="AdminMenuMarketingCommunicationsEmailTemplates"> + <data key="pageTitle">Email Templates</data> + <data key="title">Email Templates</data> + <data key="dataUiId">magento-email-template</data> + </entity> +</entities> diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml new file mode 100644 index 0000000000000..d512fc263ef2c --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingEmailTemplatesNavigateMenuTest"> + <annotations> + <features value="Email"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing email templates navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Email Templates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14173"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingEmailTemplatesPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsEmailTemplates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsEmailTemplates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml new file mode 100644 index 0000000000000..c3870417fa5e0 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml @@ -0,0 +1,40 @@ +<?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="TransactionalEmailsLogoUploadTest"> + <annotations> + <features value="Email"/> + <stories value="Email"/> + <title value="MC-13908: Uploading a Transactional Emails logo"/> + <description value="Transactional Emails Logo should be able to be uploaded in the admin and previewed"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13908"/> + <group value="LogoUpload"/> + </annotations> + <!--Login to Admin Area--> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + <!--Logout from Admin Area--> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Navigate to content->Design->Config page--> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage" /> + <waitForPageLoad stepKey="waitForPageloadToViewDesignConfigPage"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadToOpenStoreViewEditPage"/> + <!--Click Upload logo in Transactional Emails and upload the image and preview it--> + <click selector="{{AdminDesignConfigSection.logoWrapperOpen}}" stepKey="openTab" /> + <attachFile selector="{{AdminDesignConfigSection.logoUpload}}" userInput="{{MagentoLogo.file}}" stepKey="attachLogo"/> + <wait time="5" stepKey="waitingForLogoToUpload" /> + <seeElement selector="{{AdminDesignConfigSection.logoPreview}}" stepKey="LogoPreviewIsVisible"/> + </test> +</tests> diff --git a/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml index 91c38c92dc754..76a914d10b27d 100644 --- a/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml @@ -13,7 +13,7 @@ <collapsible>true</collapsible> <label translate="true">Transactional Emails</label> </settings> - <field name="email_logo" formElement="fileUploader"> + <field name="email_logo" formElement="imageUploader"> <settings> <notice translate="true">To optimize logo for high-resolution displays, upload an image that is 3x normal size and then specify 1x dimensions in the width/height fields below.</notice> <label translate="true">Logo Image</label> diff --git a/app/code/Magento/Fedex/Model/Carrier.php b/app/code/Magento/Fedex/Model/Carrier.php index 955345851e67a..0cbd90150734b 100644 --- a/app/code/Magento/Fedex/Model/Carrier.php +++ b/app/code/Magento/Fedex/Model/Carrier.php @@ -363,7 +363,6 @@ public function setRequest(RateRequest $request) if ($request->getDestPostcode()) { $r->setDestPostal($request->getDestPostcode()); - } else { } if ($request->getDestCity()) { diff --git a/app/code/Magento/Fedex/Plugin/Block/DataProviders/Tracking/ChangeTitle.php b/app/code/Magento/Fedex/Plugin/Block/DataProviders/Tracking/ChangeTitle.php new file mode 100644 index 0000000000000..86a576f2db650 --- /dev/null +++ b/app/code/Magento/Fedex/Plugin/Block/DataProviders/Tracking/ChangeTitle.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Fedex\Plugin\Block\DataProviders\Tracking; + +use Magento\Fedex\Model\Carrier; +use Magento\Shipping\Model\Tracking\Result\Status; +use Magento\Shipping\Block\DataProviders\Tracking\DeliveryDateTitle as Subject; + +/** + * Plugin to change delivery date title with FedEx customized value + */ +class ChangeTitle +{ + /** + * Title modification in case if FedEx used as carrier + * + * @param Subject $subject + * @param \Magento\Framework\Phrase|string $result + * @param Status $trackingStatus + * @return \Magento\Framework\Phrase|string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetTitle(Subject $subject, $result, Status $trackingStatus) + { + if ($trackingStatus->getCarrier() === Carrier::CODE) { + $result = __('Expected Delivery:'); + } + return $result; + } +} diff --git a/app/code/Magento/Fedex/Plugin/Block/Tracking/PopupDeliveryDate.php b/app/code/Magento/Fedex/Plugin/Block/Tracking/PopupDeliveryDate.php new file mode 100644 index 0000000000000..e1597707f9d02 --- /dev/null +++ b/app/code/Magento/Fedex/Plugin/Block/Tracking/PopupDeliveryDate.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Fedex\Plugin\Block\Tracking; + +use Magento\Shipping\Block\Tracking\Popup; +use Magento\Fedex\Model\Carrier; +use Magento\Shipping\Model\Tracking\Result\Status; + +/** + * Plugin to update delivery date value in case if Fedex used + */ +class PopupDeliveryDate +{ + /** + * Show only date for expected delivery in case if Fedex is a carrier + * + * @param Popup $subject + * @param string $result + * @param string $date + * @param string $time + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterFormatDeliveryDateTime(Popup $subject, $result, $date, $time) + { + if ($this->getCarrier($subject) === Carrier::CODE) { + $result = $subject->formatDeliveryDate($date); + } + return $result; + } + + /** + * Retrieve carrier name from tracking info + * + * @param Popup $subject + * @return string + */ + private function getCarrier(Popup $subject): string + { + foreach ($subject->getTrackingInfo() as $trackingData) { + foreach ($trackingData as $trackingInfo) { + if ($trackingInfo instanceof Status) { + $carrier = $trackingInfo->getCarrier(); + return $carrier; + } + } + } + return ''; + } +} diff --git a/app/code/Magento/Fedex/etc/di.xml b/app/code/Magento/Fedex/etc/di.xml index f17f8f2afe663..c542b1f04d1eb 100644 --- a/app/code/Magento/Fedex/etc/di.xml +++ b/app/code/Magento/Fedex/etc/di.xml @@ -22,4 +22,10 @@ </argument> </arguments> </type> + <type name="Magento\Shipping\Block\DataProviders\Tracking\DeliveryDateTitle"> + <plugin name="update_delivery_date_title" type="Magento\Fedex\Plugin\Block\DataProviders\Tracking\ChangeTitle"/> + </type> + <type name="Magento\Shipping\Block\Tracking\Popup"> + <plugin name="update_delivery_date_value" type="Magento\Fedex\Plugin\Block\Tracking\PopupDeliveryDate"/> + </type> </config> diff --git a/app/code/Magento/GiftMessage/Block/Adminhtml/Sales/Order/View/Items.php b/app/code/Magento/GiftMessage/Block/Adminhtml/Sales/Order/View/Items.php index c923ced8918d2..c15b76583187a 100644 --- a/app/code/Magento/GiftMessage/Block/Adminhtml/Sales/Order/View/Items.php +++ b/app/code/Magento/GiftMessage/Block/Adminhtml/Sales/Order/View/Items.php @@ -171,7 +171,7 @@ public function getMessage() /** * Retrieve save url * - * @return array + * @return string * @codeCoverageIgnore */ public function getSaveUrl() diff --git a/app/code/Magento/GiftMessage/Block/Cart/GiftOptions.php b/app/code/Magento/GiftMessage/Block/Cart/GiftOptions.php index 5da8b3b55700e..28a6baa436ef6 100644 --- a/app/code/Magento/GiftMessage/Block/Cart/GiftOptions.php +++ b/app/code/Magento/GiftMessage/Block/Cart/GiftOptions.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\GiftMessage\Block\Cart; use Magento\Backend\Block\Template\Context; @@ -10,6 +11,8 @@ use Magento\GiftMessage\Model\CompositeConfigProvider; /** + * Gift options cart block. + * * @api * @since 100.0.2 */ @@ -63,6 +66,8 @@ public function __construct( } /** + * Retrieve encoded js layout. + * * @return string */ public function getJsLayout() @@ -76,7 +81,7 @@ public function getJsLayout() /** * Retrieve gift message configuration * - * @return array + * @return string */ public function getGiftOptionsConfigJson() { diff --git a/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php b/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php index 0fdce9e9090ac..cb370c27863ca 100644 --- a/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php +++ b/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php @@ -7,6 +7,9 @@ use Magento\Checkout\Model\ConfigProviderInterface; +/** + * Class CompositeConfigProvider + */ class CompositeConfigProvider implements ConfigProviderInterface { /** @@ -18,13 +21,13 @@ class CompositeConfigProvider implements ConfigProviderInterface * @param ConfigProviderInterface[] $configProviders */ public function __construct( - array $configProviders + array $configProviders = [] ) { $this->configProviders = $configProviders; } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { diff --git a/app/code/Magento/GiftMessage/Model/Type/Plugin/Onepage.php b/app/code/Magento/GiftMessage/Model/Type/Plugin/Onepage.php index adb500a818517..e1c8c0b5bf5a5 100644 --- a/app/code/Magento/GiftMessage/Model/Type/Plugin/Onepage.php +++ b/app/code/Magento/GiftMessage/Model/Type/Plugin/Onepage.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\GiftMessage\Model\Type\Plugin; +/** + * Add gift message to quote plugin. + */ class Onepage { /** @@ -30,9 +34,11 @@ public function __construct( } /** + * Add gift message ot quote. + * * @param \Magento\Checkout\Model\Type\Onepage $subject * @param array $result - * @return $this + * @return array */ public function afterSaveShippingMethod( \Magento\Checkout\Model\Type\Onepage $subject, diff --git a/app/code/Magento/GoogleAnalytics/Block/Ga.php b/app/code/Magento/GoogleAnalytics/Block/Ga.php index 7d065ea50b369..b5917407b60ae 100644 --- a/app/code/Magento/GoogleAnalytics/Block/Ga.php +++ b/app/code/Magento/GoogleAnalytics/Block/Ga.php @@ -75,7 +75,8 @@ public function getPageName() } /** - * Render regular page tracking javascript code + * Render regular page tracking javascript code. + * * The custom "page name" may be set from layout or somewhere else. It must start from slash. * * @param string $accountId diff --git a/app/code/Magento/GoogleAnalytics/Helper/Data.php b/app/code/Magento/GoogleAnalytics/Helper/Data.php index 2af03c71fb1b0..90a207921d51f 100644 --- a/app/code/Magento/GoogleAnalytics/Helper/Data.php +++ b/app/code/Magento/GoogleAnalytics/Helper/Data.php @@ -46,6 +46,6 @@ public function isGoogleAnalyticsAvailable($store = null) */ public function isAnonymizedIpActive($store = null) { - return $this->scopeConfig->getValue(self::XML_PATH_ANONYMIZE, ScopeInterface::SCOPE_STORE, $store); + return (bool)$this->scopeConfig->getValue(self::XML_PATH_ANONYMIZE, ScopeInterface::SCOPE_STORE, $store); } } diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php index c4a0b55de9bfc..9e27ca5d608f0 100644 --- a/app/code/Magento/GraphQl/Controller/GraphQl.php +++ b/app/code/Magento/GraphQl/Controller/GraphQl.php @@ -12,6 +12,7 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\App\ResponseInterface; use Magento\Framework\GraphQl\Exception\ExceptionFormatter; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\QueryProcessor; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface; @@ -47,12 +48,12 @@ class GraphQl implements FrontControllerInterface private $queryProcessor; /** - * @var \Magento\Framework\GraphQl\Exception\ExceptionFormatter + * @var ExceptionFormatter */ private $graphQlError; /** - * @var \Magento\Framework\GraphQl\Query\Resolver\ContextInterface + * @var ContextInterface */ private $resolverContext; @@ -71,8 +72,8 @@ class GraphQl implements FrontControllerInterface * @param SchemaGeneratorInterface $schemaGenerator * @param SerializerInterface $jsonSerializer * @param QueryProcessor $queryProcessor - * @param \Magento\Framework\GraphQl\Exception\ExceptionFormatter $graphQlError - * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $resolverContext + * @param ExceptionFormatter $graphQlError + * @param ContextInterface $resolverContext * @param HttpRequestProcessor $requestProcessor * @param QueryFields $queryFields */ @@ -107,21 +108,23 @@ public function dispatch(RequestInterface $request) : ResponseInterface $statusCode = 200; try { /** @var Http $request */ + $this->requestProcessor->validateRequest($request); $this->requestProcessor->processHeaders($request); - $data = $this->jsonSerializer->unserialize($request->getContent()); - $query = isset($data['query']) ? $data['query'] : ''; + $data = $this->getDataFromRequest($request); + $query = $data['query'] ?? ''; + $variables = $data['variables'] ?? null; - // We have to extract queried field names to avoid instantiation of non necessary fields in webonyx schema + // We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema // Temporal coupling is required for performance optimization - $this->queryFields->setQuery($query); + $this->queryFields->setQuery($query, $variables); $schema = $this->schemaGenerator->generate(); $result = $this->queryProcessor->process( $schema, $query, $this->resolverContext, - isset($data['variables']) ? $data['variables'] : [] + $data['variables'] ?? [] ); } catch (\Exception $error) { $result['errors'] = isset($result) && isset($result['errors']) ? $result['errors'] : []; @@ -134,4 +137,26 @@ public function dispatch(RequestInterface $request) : ResponseInterface )->setHttpResponseCode($statusCode); return $this->response; } + + /** + * Get data from request body or query string + * + * @param RequestInterface $request + * @return array + */ + private function getDataFromRequest(RequestInterface $request) : array + { + /** @var Http $request */ + if ($request->isPost()) { + $data = $this->jsonSerializer->unserialize($request->getContent()); + } elseif ($request->isGet()) { + $data = $request->getParams(); + $data['variables'] = isset($data['variables']) ? + $this->jsonSerializer->unserialize($data['variables']) : null; + } else { + return []; + } + + return $data; + } } diff --git a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php b/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php deleted file mode 100644 index 2270f2616e67b..0000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Controller\HttpHeaderProcessor; - -use Magento\Framework\Exception\LocalizedException; -use Magento\GraphQl\Controller\HttpHeaderProcessorInterface; - -/** - * Processes the "Content-Type" header entry - */ -class ContentTypeProcessor implements HttpHeaderProcessorInterface -{ - /** - * Handle the mandatory application/json header - * - * {@inheritDoc} - * @throws LocalizedException - */ - public function processHeaderValue(string $headerValue) : void - { - if (!$headerValue || strpos($headerValue, 'application/json') === false) { - throw new LocalizedException( - new \Magento\Framework\Phrase('Request content type must be application/json') - ); - } - } -} diff --git a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php b/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php index be359eafdf246..246ad15379f85 100644 --- a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php +++ b/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php @@ -7,7 +7,7 @@ namespace Magento\GraphQl\Controller\HttpHeaderProcessor; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\HttpRequestInterface; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Controller\HttpHeaderProcessorInterface; use Magento\Store\Model\StoreManagerInterface; @@ -35,8 +35,9 @@ public function __construct(StoreManagerInterface $storeManager) /** * Handle the value of the store and set the scope * - * {@inheritDoc} - * @throws NoSuchEntityException + * @param string $headerValue + * @return void + * @throws GraphQlInputException */ public function processHeaderValue(string $headerValue) : void { diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php b/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php index 5e42b81a61981..bb29f1fa68af9 100644 --- a/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php +++ b/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php @@ -19,12 +19,19 @@ class HttpRequestProcessor */ private $headerProcessors = []; + /** + * @var HttpRequestValidatorInterface[] array + */ + private $requestValidators = []; + /** * @param HttpHeaderProcessorInterface[] $graphQlHeaders + * @param HttpRequestValidatorInterface[] $requestValidators */ - public function __construct(array $graphQlHeaders = []) + public function __construct(array $graphQlHeaders = [], array $requestValidators = []) { $this->headerProcessors = $graphQlHeaders; + $this->requestValidators = $requestValidators; } /** @@ -39,4 +46,17 @@ public function processHeaders(Http $request) : void $headerClass->processHeaderValue((string)$request->getHeader($headerName)); } } + + /** + * Validate HTTP request + * + * @param Http $request + * @return void + */ + public function validateRequest(Http $request) : void + { + foreach ($this->requestValidators as $requestValidator) { + $requestValidator->validate($request); + } + } } diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/ContentTypeValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/ContentTypeValidator.php new file mode 100644 index 0000000000000..555048aac6771 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/ContentTypeValidator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpRequestValidator; + +use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GraphQl\Controller\HttpRequestValidatorInterface; + +/** + * Processes the "Content-Type" header entry + */ +class ContentTypeValidator implements HttpRequestValidatorInterface +{ + /** + * Handle the mandatory application/json header + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request) : void + { + $headerName = 'Content-Type'; + $requiredHeaderValue = 'application/json'; + + $headerValue = (string)$request->getHeader($headerName); + if ($request->isPost() + && strpos($headerValue, $requiredHeaderValue) === false + ) { + throw new GraphQlInputException( + new \Magento\Framework\Phrase('Request content type must be application/json') + ); + } + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php new file mode 100644 index 0000000000000..300b3d4f44dca --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpRequestValidator; + +use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\HttpRequestValidatorInterface; + +/** + * Validator to check HTTP verb for Graphql requests + */ +class HttpVerbValidator implements HttpRequestValidatorInterface +{ + /** + * Check if request is using correct verb for query or mutation + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request) : void + { + /** @var Http $request */ + if (false === $request->isPost()) { + $query = $request->getParam('query', ''); + // The easiest way to determine mutations without additional parsing + if (strpos(trim($query), 'mutation') === 0) { + throw new GraphQlInputException( + new \Magento\Framework\Phrase('Mutation requests allowed only for POST requests') + ); + } + } + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidatorInterface.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidatorInterface.php new file mode 100644 index 0000000000000..c0873b0caff89 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidatorInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller; + +use Magento\Framework\App\HttpRequestInterface; + +/** + * Use this interface to implement a validator for a Graphql HTTP requests + */ +interface HttpRequestValidatorInterface +{ + /** + * Perform validation of request + * + * @param HttpRequestInterface $request + * @return void + */ + public function validate(HttpRequestInterface $request) : void; +} diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index b2083ea758e56..6acb78f9c7f9e 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -27,7 +27,7 @@ <argument name="factoryMapByConfigElementType" xsi:type="array"> <item name="graphql_interface" xsi:type="object">Magento\Framework\GraphQl\Config\Element\InterfaceFactory</item> <item name="graphql_type" xsi:type="object">Magento\Framework\GraphQl\Config\Element\TypeFactory</item> - <item name="graphql_input" xsi:type="object">Magento\Framework\GraphQl\Config\Element\TypeFactory</item> + <item name="graphql_input" xsi:type="object">Magento\Framework\GraphQl\Config\Element\InputFactory</item> <item name="graphql_enum" xsi:type="object">Magento\Framework\GraphQl\Config\Element\EnumFactory</item> </argument> </arguments> @@ -55,24 +55,16 @@ </argument> </arguments> </virtualType> - <type name="Magento\Framework\GraphQl\Schema\Type\Output\OutputFactory"> + <type name="Magento\Framework\GraphQl\Schema\Type\TypeRegistry"> <arguments> - <argument name="prototypes" xsi:type="array"> + <argument name="configToTypeMap" xsi:type="array"> <item name="Magento\Framework\GraphQl\Config\Element\Type" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Output\OutputTypeObject</item> + <item name="Magento\Framework\GraphQl\Config\Element\Input" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Input\InputObjectType</item> <item name="Magento\Framework\GraphQl\Config\Element\InterfaceType" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Output\OutputInterfaceObject</item> <item name="Magento\Framework\GraphQl\Config\Element\Enum" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Enum\Enum</item> </argument> </arguments> </type> - <type name="Magento\Framework\GraphQl\Schema\Type\Input\InputFactory"> - <arguments> - <argument name="prototypes" xsi:type="array"> - <item name="Magento\Framework\GraphQl\Config\Element\Type" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Input\InputObjectType</item> - <item name="Magento\Framework\GraphQl\Config\Element\InterfaceType" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Input\InputObjectType</item> - <item name="Magento\Framework\GraphQl\Config\Element\Enum" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Enum\Enum</item> - </argument> - </arguments> - </type> <type name="Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper"> <arguments> <argument name="formatter" xsi:type="object">Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\FormatterComposite</argument> diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index f4e6ca59364b2..b4f0113f58776 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -28,9 +28,12 @@ <type name="Magento\GraphQl\Controller\HttpRequestProcessor"> <arguments> <argument name="graphQlHeaders" xsi:type="array"> - <item name="Content-Type" xsi:type="object">Magento\GraphQl\Controller\HttpHeaderProcessor\ContentTypeProcessor</item> <item name="Store" xsi:type="object">Magento\GraphQl\Controller\HttpHeaderProcessor\StoreProcessor</item> </argument> + <argument name="requestValidators" xsi:type="array"> + <item name="ContentTypeValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\ContentTypeValidator</item> + <item name="VerbValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator</item> + </argument> </arguments> </type> </config> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 2281495d059e1..7ea715097cdf3 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -5,7 +5,6 @@ type Query { } type Mutation { - placeholderMutation: String @doc(description: "Mutation type cannot be declared without fields. The placeholder will be removed when at least one mutation field is declared.") } input FilterTypeInput @doc(description: "FilterTypeInput specifies which action will be performed in a query ") { diff --git a/app/code/Magento/GroupedImportExport/etc/di.xml b/app/code/Magento/GroupedImportExport/etc/di.xml index 38030b3ec94eb..25fd3b5697514 100644 --- a/app/code/Magento/GroupedImportExport/etc/di.xml +++ b/app/code/Magento/GroupedImportExport/etc/di.xml @@ -9,7 +9,7 @@ <type name="Magento\CatalogImportExport\Model\Export\RowCustomizer\Composite"> <arguments> <argument name="customizers" xsi:type="array"> - <item name="gropedProduct" xsi:type="string">Magento\GroupedImportExport\Model\Export\RowCustomizer</item> + <item name="groupedProduct" xsi:type="string">Magento\GroupedImportExport\Model\Export\RowCustomizer</item> </argument> </arguments> </type> diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index f67c9c57ee034..187fd27fa0554 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -210,7 +210,7 @@ public function getAssociatedProducts($product) $collection = $this->getAssociatedProductCollection( $product )->addAttributeToSelect( - ['name', 'price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id'] + ['name', 'price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id', 'image'] )->addFilterByRequiredOptions()->setPositionOrder()->addStoreFilter( $this->getStoreFilter($product) )->addAttributeToFilter( @@ -475,10 +475,12 @@ public function hasWeight() * @param \Magento\Catalog\Model\Product $product * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product) { } + //phpcs:enable /** * @inheritdoc @@ -488,6 +490,7 @@ public function beforeSave($product) //clear cached associated links $product->unsetData($this->_keyAssociatedProducts); if ($product->hasData('product_options') && !empty($product->getData('product_options'))) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Custom options for grouped product type are not supported'); } return parent::beforeSave($product); diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php index 8d1548036cd3e..cbe1ef26c54b0 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php @@ -8,7 +8,10 @@ namespace Magento\GroupedProduct\Model\ResourceModel\Product\Type\Grouped; /** + * Associated products collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AssociatedProductsCollection extends \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection { diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 0000000000000..5937267b4a61e --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ +<?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="VerifyProductTypeOrder"> + <seeElement stepKey="seeGroupedInOrder" selector="{{AdminProductDropdownOrderSection.groupedProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml index 4d979953a934e..ba3703e7b0edc 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml @@ -18,7 +18,7 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> </entity> <entity name="ApiGroupedProduct" type="product3"> - <data key="sku" unique="suffix">api-grouped-product</data> + <data key="sku" unique="suffix">apiGroupedProduct</data> <data key="type_id">grouped</data> <data key="attribute_set_id">4</data> <data key="name" unique="suffix">Api Grouped Product</data> @@ -28,4 +28,16 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiGroupedProduct2" type="product3"> + <data key="sku" unique="suffix">apiGroupedProduct</data> + <data key="type_id">grouped</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">Api Grouped Product</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-grouped-product</data> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 0000000000000..3efbabd34c1a4 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.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="AdminProductDropdownOrderSection"> + <element name="groupedProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Virtual Product']]) and not(preceding-sibling::li[span[@title='Bundle Product']]) and not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']]) and not(following-sibling::li[span[@title='Configurable Product']])]/span[@title='Grouped Product']"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml index 8634fc3f6f9dc..c3a95bbef3aa3 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoGroupedProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml new file mode 100644 index 0000000000000..966e24851395c --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml @@ -0,0 +1,57 @@ +<?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="AdminDeleteGroupedProductTest"> + <annotations> + <features value="GroupedProduct"/> + <title value="Delete Grouped Product"/> + <description value="Admin should be able to delete a grouped product"/> + <testCaseId value="MC-11019"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteGroupedProductFilteredBySkuAndName"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createGroupedProduct.name$$)}}" stepKey="amOnGroupedProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!--Search for the product by sku--> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createGroupedProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createGroupedProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createGroupedProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml index 25c45abdfe047..e322d4a1eb038 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoGroupedProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml index 0fd52ac4a65a4..2a600d38250f8 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchGroupedProductByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php index f02849c244cb3..176c29add4837 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php @@ -64,7 +64,7 @@ public function testGetFinalPrice( $expectedFinalPrice ) { $rawFinalPrice = 10; - $rawPriceCheckStep = 6; + $rawPriceCheckStep = 5; $this->productMock->expects( $this->any() @@ -155,7 +155,7 @@ public function getFinalPriceDataProvider() 'custom_option_null' => [ 'associatedProducts' => [], 'options' => [[], []], - 'expectedPriceCall' => 6, /* product call number to check final price formed correctly */ + 'expectedPriceCall' => 5, /* product call number to check final price formed correctly */ 'expectedFinalPrice' => 10, /* 10(product price) + 2(options count) * 5(qty) * 5(option price) */ ], 'custom_option_exist' => [ @@ -165,7 +165,7 @@ public function getFinalPriceDataProvider() ['associated_product_2', $optionMock], ['associated_product_3', $optionMock], ], - 'expectedPriceCall' => 16, /* product call number to check final price formed correctly */ + 'expectedPriceCall' => 15, /* product call number to check final price formed correctly */ 'expectedFinalPrice' => 35, /* 10(product price) + 2(options count) * 5(qty) * 5(option price) */ ] ]; diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php index 327b47d4a75d8..ad4b86351a66c 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php @@ -21,6 +21,9 @@ use Magento\GroupedProduct\Model\Product\Type\Grouped as GroupedProductType; use Magento\GroupedProduct\Ui\DataProvider\Product\Form\Modifier\Grouped; use Magento\Store\Api\Data\StoreInterface; +use Magento\Catalog\Model\Product; +use Magento\GroupedProduct\Model\Product\Link\CollectionProvider\Grouped as GroupedProducts; +use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; /** * Class GroupedTest @@ -82,23 +85,38 @@ class GroupedTest extends AbstractModifierTest */ protected $storeMock; + /** + * @var GroupedProducts|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupedProductsMock; + + /** + * @var ProductLinkInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $productLinkFactoryMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = new ObjectManager($this); $this->locatorMock = $this->getMockBuilder(LocatorInterface::class) ->getMockForAbstractClass(); - $this->productMock = $this->getMockBuilder(ProductInterface::class) + $this->productMock = $this->getMockBuilder(Product::class) ->setMethods(['getId', 'getTypeId']) - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMock(); $this->productMock->expects($this->any()) ->method('getId') ->willReturn(self::PRODUCT_ID); $this->productMock->expects($this->any()) ->method('getTypeId') ->willReturn(GroupedProductType::TYPE_CODE); - $this->linkedProductMock = $this->getMockBuilder(ProductInterface::class) + $this->linkedProductMock = $this->getMockBuilder(Product::class) ->setMethods(['getId', 'getName', 'getPrice']) - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMock(); $this->linkedProductMock->expects($this->any()) ->method('getId') ->willReturn(self::LINKED_PRODUCT_ID); @@ -135,7 +153,7 @@ protected function setUp() $this->linkRepositoryMock->expects($this->any()) ->method('getList') ->with($this->productMock) - ->willReturn([$this->linkMock]); + ->willReturn([$this->linkedProductMock]); $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) ->setMethods(['get']) ->getMockForAbstractClass(); @@ -155,7 +173,7 @@ protected function setUp() } /** - * {@inheritdoc} + * @inheritdoc */ protected function createModel() { @@ -169,6 +187,16 @@ protected function createModel() ->setMethods(['init', 'getUrl']) ->disableOriginalConstructor() ->getMock(); + + $this->groupedProductsMock = $this->getMockBuilder(GroupedProducts::class) + ->setMethods(['getLinkedProducts']) + ->disableOriginalConstructor() + ->getMock(); + $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->imageHelperMock->expects($this->any()) ->method('init') ->willReturn($this->imageHelperMock); @@ -189,16 +217,23 @@ protected function createModel() 'localeCurrency' => $this->currencyMock, 'imageHelper' => $this->imageHelperMock, 'attributeSetRepository' => $this->attributeSetRepositoryMock, + 'groupedProducts' => $this->groupedProductsMock, + 'productLinkFactory' => $this->productLinkFactoryMock, ]); } + /** + * Assert array has key + * + * @return void + */ public function testModifyMeta() { $this->assertArrayHasKey(Grouped::GROUP_GROUPED, $this->getModel()->modifyMeta([])); } /** - * {@inheritdoc} + * @inheritdoc */ public function testModifyData() { @@ -226,6 +261,42 @@ public function testModifyData() ], ], ]; - $this->assertSame($expectedData, $this->getModel()->modifyData([])); + $model = $this->getModel(); + $linkedProductMock = $this->getMockBuilder(Product::class) + ->setMethods(['getId', 'getName', 'getPrice', 'getSku', 'getImage', 'getPosition', 'getQty']) + ->disableOriginalConstructor() + ->getMock(); + $linkedProductMock->expects($this->once()) + ->method('getId') + ->willReturn(self::LINKED_PRODUCT_ID); + $linkedProductMock->expects($this->once()) + ->method('getName') + ->willReturn(self::LINKED_PRODUCT_NAME); + $linkedProductMock->expects($this->once()) + ->method('getPrice') + ->willReturn(self::LINKED_PRODUCT_PRICE); + $linkedProductMock->expects($this->once()) + ->method('getSku') + ->willReturn(self::LINKED_PRODUCT_SKU); + $linkedProductMock->expects($this->once()) + ->method('getImage') + ->willReturn(''); + $linkedProductMock->expects($this->exactly(2)) + ->method('getPosition') + ->willReturn(self::LINKED_PRODUCT_POSITION); + $linkedProductMock->expects($this->once()) + ->method('getQty') + ->willReturn(self::LINKED_PRODUCT_QTY); + $this->groupedProductsMock->expects($this->once()) + ->method('getLinkedProducts') + ->willReturn([$linkedProductMock]); + $linkMock = $this->getMockBuilder(ProductLinkInterface::class) + ->getMockForAbstractClass(); + + $this->productLinkFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($linkMock); + + $this->assertSame($expectedData, $model->modifyData([])); } } 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 57d9bc78aaf28..2ea622c1c2b8f 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 @@ -21,6 +21,9 @@ use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Locale\CurrencyInterface; +use Magento\GroupedProduct\Model\Product\Link\CollectionProvider\Grouped as GroupedProducts; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; /** * Data provider for Grouped products @@ -99,6 +102,16 @@ class Grouped extends AbstractModifier */ private static $codeQty = 'qty'; + /** + * @var GroupedProducts + */ + private $groupedProducts; + + /** + * @var ProductLinkInterfaceFactory + */ + private $productLinkFactory; + /** * @param LocatorInterface $locator * @param UrlInterface $urlBuilder @@ -109,6 +122,9 @@ class Grouped extends AbstractModifier * @param AttributeSetRepositoryInterface $attributeSetRepository * @param CurrencyInterface $localeCurrency * @param array $uiComponentsConfig + * @param GroupedProducts $groupedProducts + * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( LocatorInterface $locator, @@ -119,7 +135,9 @@ public function __construct( Status $status, AttributeSetRepositoryInterface $attributeSetRepository, CurrencyInterface $localeCurrency, - array $uiComponentsConfig = [] + array $uiComponentsConfig = [], + GroupedProducts $groupedProducts = null, + \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null ) { $this->locator = $locator; $this->urlBuilder = $urlBuilder; @@ -130,6 +148,11 @@ public function __construct( $this->status = $status; $this->localeCurrency = $localeCurrency; $this->uiComponentsConfig = array_replace_recursive($this->uiComponentsConfig, $uiComponentsConfig); + $this->groupedProducts = $groupedProducts ?: ObjectManager::getInstance()->get( + \Magento\GroupedProduct\Model\Product\Link\CollectionProvider\Grouped::class + ); + $this->productLinkFactory = $productLinkFactory ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); } /** @@ -143,18 +166,15 @@ public function modifyData(array $data) if ($modelId) { $storeId = $this->locator->getStore()->getId(); $data[$product->getId()]['links'][self::LINK_TYPE] = []; - $linkedItems = $this->productLinkRepository->getList($product); + $linkedItems = $this->groupedProducts->getLinkedProducts($product); usort($linkedItems, function ($a, $b) { return $a->getPosition() <=> $b->getPosition(); }); + $productLink = $this->productLinkFactory->create(); foreach ($linkedItems as $index => $linkItem) { - if ($linkItem->getLinkType() !== self::LINK_TYPE) { - continue; - } /** @var \Magento\Catalog\Api\Data\ProductInterface $linkedProduct */ - $linkedProduct = $this->productRepository->get($linkItem->getLinkedProductSku(), false, $storeId); $linkItem->setPosition($index); - $data[$modelId]['links'][self::LINK_TYPE][] = $this->fillData($linkedProduct, $linkItem); + $data[$modelId]['links'][self::LINK_TYPE][] = $this->fillData($linkItem, $productLink); } $data[$modelId][self::DATA_SOURCE_DEFAULT]['current_store_id'] = $storeId; } @@ -167,6 +187,7 @@ public function modifyData(array $data) * @param ProductInterface $linkedProduct * @param ProductLinkInterface $linkItem * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterface $linkItem) { @@ -176,12 +197,15 @@ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterfac return [ 'id' => $linkedProduct->getId(), 'name' => $linkedProduct->getName(), - 'sku' => $linkItem->getLinkedProductSku(), + 'sku' => $linkedProduct->getSku(), 'price' => $currency->toCurrency(sprintf("%f", $linkedProduct->getPrice())), - 'qty' => $linkItem->getExtensionAttributes()->getQty(), - 'position' => $linkItem->getPosition(), - 'positionCalculated' => $linkItem->getPosition(), - 'thumbnail' => $this->imageHelper->init($linkedProduct, 'product_listing_thumbnail')->getUrl(), + 'qty' => $linkedProduct->getQty(), + 'position' => $linkedProduct->getPosition(), + 'positionCalculated' => $linkedProduct->getPosition(), + 'thumbnail' => $this->imageHelper + ->init($linkedProduct, 'product_listing_thumbnail') + ->setImageFile($linkedProduct->getImage()) + ->getUrl(), 'type_id' => $linkedProduct->getTypeId(), 'status' => $this->status->getOptionText($linkedProduct->getStatus()), 'attribute_set' => $this->attributeSetRepository @@ -414,8 +438,8 @@ protected function getButtonSet() 'component' => 'Magento_Ui/js/form/components/button', 'actions' => [ [ - 'targetName' => - $this->uiComponentsConfig['form'] . '.' . $this->uiComponentsConfig['form'] + 'targetName' => $this->uiComponentsConfig['form'] . + '.' . $this->uiComponentsConfig['form'] . '.' . static::GROUP_GROUPED . '.' @@ -423,8 +447,8 @@ protected function getButtonSet() 'actionName' => 'openModal', ], [ - 'targetName' => - $this->uiComponentsConfig['form'] . '.' . $this->uiComponentsConfig['form'] + 'targetName' => $this->uiComponentsConfig['form'] . + '.' . $this->uiComponentsConfig['form'] . '.' . static::GROUP_GROUPED . '.' diff --git a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml index 900d4a1bd5bbc..0be71f20a3822 100644 --- a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml +++ b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml @@ -33,8 +33,8 @@ </thead> <?php if ($_hasAssociatedProducts): ?> - <?php foreach ($_associatedProducts as $_item): ?> <tbody> + <?php foreach ($_associatedProducts as $_item): ?> <tr> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> @@ -80,8 +80,8 @@ </td> </tr> <?php endif; ?> - </tbody> <?php endforeach; ?> + </tbody> <?php else: ?> <tbody> <tr> diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php index 087cf10c8d6bb..8818766692fe2 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php @@ -8,19 +8,21 @@ namespace Magento\GroupedProductGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\GroupedProduct\Model\Product\Type\Grouped as Type; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedProductTypeResolver implements TypeResolverInterface { + const GROUPED_PRODUCT = 'GroupedProduct'; /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { - if (isset($data['type_id']) && $data['type_id'] == 'grouped') { - return 'GroupedProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { + return self::GROUPED_PRODUCT; } return ''; } diff --git a/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php b/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php new file mode 100644 index 0000000000000..01c41e35fc4eb --- /dev/null +++ b/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Api\Data; + +/** + * Basic interface with data needed for export operation. + * @api + */ +interface ExportInfoInterface +{ + /** + * Return filename. + * + * @return string + */ + public function getFileName(); + + /** + * Set filename into local variable. + * + * @param string $fileName + * @return void + */ + public function setFileName($fileName); + + /** + * Override standard entity getter. + * + * @return string + */ + public function getFileFormat(); + + /** + * Set file format. + * + * @param string $fileFormat + * @return void + */ + public function setFileFormat($fileFormat); + + /** + * Return content type. + * + * @return string + */ + public function getContentType(); + + /** + * Set content type. + * + * @param string $contentType + * @return void + */ + public function setContentType($contentType); + + /** + * Returns entity. + * + * @return string + */ + public function getEntity(); + + /** + * Set entity for export logic. + * + * @param string $entity + * @return void + */ + public function setEntity($entity); + + /** + * Returns export filter. + * + * @return string + */ + public function getExportFilter(); + + /** + * Set filter for export result. + * + * @param string $exportFilter + * @return void + */ + public function setExportFilter($exportFilter); +} diff --git a/app/code/Magento/ImportExport/Api/ExportManagementInterface.php b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php new file mode 100644 index 0000000000000..39bb89b43c838 --- /dev/null +++ b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Api; + +use Magento\ImportExport\Api\Data\ExportInfoInterface; + +/** + * Describes how to do export operation with data interface. + * @api + */ +interface ExportManagementInterface +{ + /** + * Return export data. + * + * @param ExportInfoInterface $exportInfo + * @return string + */ + public function export(ExportInfoInterface $exportInfo); +} diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php b/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php index 8721dd05a0fa6..d032f2f7621b2 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php @@ -237,8 +237,8 @@ protected function _getSelectHtmlWithValue(Attribute $attribute, $value) if ($attribute->getFilterOptions()) { $options = []; - foreach ($attribute->getFilterOptions() as $value => $label) { - $options[] = ['value' => $value, 'label' => $label]; + foreach ($attribute->getFilterOptions() as $optionValue => $label) { + $options[] = ['value' => $optionValue, 'label' => $label]; } } else { $options = $attribute->getSource()->getAllOptions(false); diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index 68b4d849099c1..d6b96a28afcc9 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -18,7 +18,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic /** * Basic import model * - * @var \Magento\ImportExport\Model\Import + * @var Import */ protected $_importModel; @@ -77,8 +77,10 @@ protected function _prepareForm() ); // base fieldset - $fieldsets['base'] = $form->addFieldset('base_fieldset', ['legend' => __('Import Settings')]); - $fieldsets['base']->addField( + $fieldsets['base'] = $form->addFieldset( + 'base_fieldset', + ['legend' => __('Import Settings')] + )->addField( 'entity', 'select', [ @@ -95,12 +97,11 @@ protected function _prepareForm() // add behaviour fieldsets $uniqueBehaviors = $this->_importModel->getUniqueEntityBehaviors(); foreach ($uniqueBehaviors as $behaviorCode => $behaviorClass) { - $fieldsets[$behaviorCode] = $form->addFieldset( + $fieldset = $form->addFieldset( $behaviorCode . '_fieldset', ['legend' => __('Import Behavior'), 'class' => 'no-display'] ); - /** @var $behaviorSource \Magento\ImportExport\Model\Source\Import\AbstractBehavior */ - $fieldsets[$behaviorCode]->addField( + $fieldset->addField( $behaviorCode, 'select', [ @@ -116,13 +117,13 @@ protected function _prepareForm() 'after_element_html' => $this->getImportBehaviorTooltip(), ] ); - $fieldsets[$behaviorCode]->addField( - $behaviorCode . \Magento\ImportExport\Model\Import::FIELD_NAME_VALIDATION_STRATEGY, + $fieldset->addField( + $behaviorCode . Import::FIELD_NAME_VALIDATION_STRATEGY, 'select', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_NAME_VALIDATION_STRATEGY, - 'title' => __(' '), - 'label' => __(' '), + 'name' => Import::FIELD_NAME_VALIDATION_STRATEGY, + 'title' => __('Validation Strategy'), + 'label' => __('Validation Strategy'), 'required' => true, 'class' => $behaviorCode, 'disabled' => true, @@ -133,11 +134,11 @@ protected function _prepareForm() 'after_element_html' => $this->getDownloadSampleFileHtml(), ] ); - $fieldsets[$behaviorCode]->addField( - $behaviorCode . '_' . \Magento\ImportExport\Model\Import::FIELD_NAME_ALLOWED_ERROR_COUNT, + $fieldset->addField( + $behaviorCode . '_' . Import::FIELD_NAME_ALLOWED_ERROR_COUNT, 'text', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_NAME_ALLOWED_ERROR_COUNT, + 'name' => Import::FIELD_NAME_ALLOWED_ERROR_COUNT, 'label' => __('Allowed Errors Count'), 'title' => __('Allowed Errors Count'), 'required' => true, @@ -149,11 +150,11 @@ protected function _prepareForm() ), ] ); - $fieldsets[$behaviorCode]->addField( - $behaviorCode . '_' . \Magento\ImportExport\Model\Import::FIELD_FIELD_SEPARATOR, + $fieldset->addField( + $behaviorCode . '_' . Import::FIELD_FIELD_SEPARATOR, 'text', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_FIELD_SEPARATOR, + 'name' => Import::FIELD_FIELD_SEPARATOR, 'label' => __('Field separator'), 'title' => __('Field separator'), 'required' => true, @@ -162,11 +163,11 @@ protected function _prepareForm() 'value' => ',', ] ); - $fieldsets[$behaviorCode]->addField( - $behaviorCode . \Magento\ImportExport\Model\Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR, + $fieldset->addField( + $behaviorCode . Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR, 'text', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR, + 'name' => Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR, 'label' => __('Multiple value separator'), 'title' => __('Multiple value separator'), 'required' => true, @@ -175,11 +176,11 @@ protected function _prepareForm() 'value' => Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, ] ); - $fieldsets[$behaviorCode]->addField( - $behaviorCode . \Magento\ImportExport\Model\Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT, + $fieldset->addField( + $behaviorCode . Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT, 'text', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT, + 'name' => Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT, 'label' => __('Empty attribute value constant'), 'title' => __('Empty attribute value constant'), 'required' => true, @@ -188,28 +189,29 @@ protected function _prepareForm() 'value' => Import::DEFAULT_EMPTY_ATTRIBUTE_VALUE_CONSTANT, ] ); - $fieldsets[$behaviorCode]->addField( - $behaviorCode . \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE, + $fieldset->addField( + $behaviorCode . Import::FIELDS_ENCLOSURE, 'checkbox', [ - 'name' => \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE, + 'name' => Import::FIELDS_ENCLOSURE, 'label' => __('Fields enclosure'), 'title' => __('Fields enclosure'), 'value' => 1, ] ); + $fieldsets[$behaviorCode] = $fieldset; } // fieldset for file uploading - $fieldsets['upload'] = $form->addFieldset( + $fieldset = $form->addFieldset( 'upload_file_fieldset', ['legend' => __('File to Import'), 'class' => 'no-display'] ); - $fieldsets['upload']->addField( - \Magento\ImportExport\Model\Import::FIELD_NAME_SOURCE_FILE, + $fieldset->addField( + Import::FIELD_NAME_SOURCE_FILE, 'file', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_NAME_SOURCE_FILE, + 'name' => Import::FIELD_NAME_SOURCE_FILE, 'label' => __('Select File to Import'), 'title' => __('Select File to Import'), 'required' => true, @@ -219,11 +221,11 @@ protected function _prepareForm() ), ] ); - $fieldsets['upload']->addField( - \Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR, + $fieldset->addField( + Import::FIELD_NAME_IMG_FILE_DIR, 'text', [ - 'name' => \Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR, + 'name' => Import::FIELD_NAME_IMG_FILE_DIR, 'label' => __('Images File Directory'), 'title' => __('Images File Directory'), 'required' => false, @@ -234,6 +236,7 @@ protected function _prepareForm() ), ] ); + $fieldsets['upload'] = $fieldset; $form->setUseContainer(true); $this->setForm($form); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php index 38bfbd88b0c12..13c22a976e798 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php @@ -11,9 +11,12 @@ use Magento\Backend\App\Action\Context; use Magento\Framework\App\Response\Http\FileFactory; use Magento\ImportExport\Model\Export as ExportModel; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\ImportExport\Model\Export\Entity\ExportInfoFactory; +/** + * Controller for export operation. + */ class Export extends ExportController implements HttpPostActionInterface { /** @@ -27,18 +30,38 @@ class Export extends ExportController implements HttpPostActionInterface private $sessionManager; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory - * @param \Magento\Framework\Session\SessionManagerInterface $sessionManager [optional] + * @var PublisherInterface + */ + private $messagePublisher; + + /** + * @var ExportInfoFactory + */ + private $exportInfoFactory; + + /** + * @param Context $context + * @param FileFactory $fileFactory + * @param \Magento\Framework\Session\SessionManagerInterface|null $sessionManager + * @param PublisherInterface|null $publisher + * @param ExportInfoFactory|null $exportInfoFactory */ public function __construct( Context $context, FileFactory $fileFactory, - \Magento\Framework\Session\SessionManagerInterface $sessionManager = null + \Magento\Framework\Session\SessionManagerInterface $sessionManager = null, + PublisherInterface $publisher = null, + ExportInfoFactory $exportInfoFactory = null ) { $this->fileFactory = $fileFactory; $this->sessionManager = $sessionManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Session\SessionManagerInterface::class); + $this->messagePublisher = $publisher ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(PublisherInterface::class); + $this->exportInfoFactory = $exportInfoFactory ?: + \Magento\Framework\App\ObjectManager::getInstance()->get( + ExportInfoFactory::class + ); parent::__construct($context); } @@ -51,19 +74,19 @@ public function execute() { if ($this->getRequest()->getPost(ExportModel::FILTER_ELEMENT_GROUP)) { try { - /** @var $model \Magento\ImportExport\Model\Export */ - $model = $this->_objectManager->create(\Magento\ImportExport\Model\Export::class); - $model->setData($this->getRequest()->getParams()); + $params = $this->getRequest()->getParams(); + + /** @var ExportInfoFactory $dataObject */ + $dataObject = $this->exportInfoFactory->create( + $params['file_format'], + $params['entity'], + $params['export_filter'] + ); - $this->sessionManager->writeClose(); - return $this->fileFactory->create( - $model->getFileName(), - $model->export(), - DirectoryList::VAR_DIR, - $model->getContentType() + $this->messagePublisher->publish('import_export.export', $dataObject); + $this->messageManager->addSuccessMessage( + __('Message is added to queue, wait to get your file soon') ); - } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); $this->messageManager->addError(__('Please correct the data sent value.')); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php new file mode 100644 index 0000000000000..6996ba90c3e10 --- /dev/null +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Controller\Adminhtml\Export\File; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; + +/** + * Controller that delete file by name. + */ +class Delete extends ExportController implements HttpGetActionInterface +{ + /** + * url to this controller + */ + const URL = 'admin/export_file/delete'; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var DriverInterface + */ + private $file; + + /** + * Delete constructor. + * @param Action\Context $context + * @param Filesystem $filesystem + * @param DriverInterface $file + */ + public function __construct( + Action\Context $context, + Filesystem $filesystem, + DriverInterface $file + ) { + $this->filesystem = $filesystem; + $this->file = $file; + parent::__construct($context); + } + + /** + * Controller basic method implementation. + * + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface + * @throws LocalizedException + */ + public function execute() + { + try { + if (empty($fileName = $this->getRequest()->getParam('filename'))) { + throw new LocalizedException(__('Please provide export file name')); + } + $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); + $path = $directory->getAbsolutePath() . 'export/' . $fileName; + $this->file->deleteFile($path); + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $resultRedirect->setPath('adminhtml/export/index'); + return $resultRedirect; + } catch (FileSystemException $exception) { + throw new LocalizedException(__('There are no export file with such name %1', $fileName)); + } + } +} diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php new file mode 100644 index 0000000000000..32385e62a5dce --- /dev/null +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Controller\Adminhtml\Export\File; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; +use Magento\Framework\Filesystem; + +/** + * Controller that download file by name. + */ +class Download extends ExportController implements HttpGetActionInterface +{ + /** + * url to this controller + */ + const URL = 'admin/export_file/download/'; + + /** + * @var FileFactory + */ + private $fileFactory; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * DownloadFile constructor. + * @param Action\Context $context + * @param FileFactory $fileFactory + * @param Filesystem $filesystem + */ + public function __construct( + Action\Context $context, + FileFactory $fileFactory, + Filesystem $filesystem + ) { + $this->fileFactory = $fileFactory; + $this->filesystem = $filesystem; + parent::__construct($context); + } + + /** + * Controller basic method implementation. + * + * @return \Magento\Framework\App\ResponseInterface + * @throws LocalizedException + */ + public function execute() + { + if (empty($fileName = $this->getRequest()->getParam('filename'))) { + throw new LocalizedException(__('Please provide export file name')); + } + try { + $path = 'export/' . $fileName; + $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); + if ($directory->isFile($path)) { + return $this->fileFactory->create( + $path, + $directory->readFile($path), + DirectoryList::VAR_DIR + ); + } + } catch (LocalizedException | \Exception $exception) { + throw new LocalizedException(__('There are no export file with such name %1', $fileName)); + } + } +} diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index a0992e28bb2cd..c18e666260898 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -86,7 +86,7 @@ private function processValidationResult($validationResult, $resultBlock) $resultBlock->addError( __('Data validation failed. Please fix the following errors and upload the file again.') ); - $this->addErrorMessages($resultBlock, $errorAggregator); + if ($errorAggregator->getErrorsCount()) { $this->addMessageToSkipErrors($resultBlock); } @@ -100,6 +100,8 @@ private function processValidationResult($validationResult, $resultBlock) $errorAggregator->getErrorsCount() ) ); + + $this->addErrorMessages($resultBlock, $errorAggregator); } else { if ($errorAggregator->getErrorsCount()) { $this->collectErrors($resultBlock); diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index 695df18fd1030..850ded7c8f256 100644 --- a/app/code/Magento/ImportExport/Model/Export.php +++ b/app/code/Magento/ImportExport/Model/Export.php @@ -13,6 +13,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * @deprecated */ class Export extends \Magento\ImportExport\Model\AbstractModel { diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php new file mode 100644 index 0000000000000..27019780269c4 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Export; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\ImportExport\Api\ExportManagementInterface; +use Magento\ImportExport\Api\Data\ExportInfoInterface; +use Magento\Framework\Notification\NotifierInterface; + +/** + * Consumer for export message. + */ +class Consumer +{ + /** + * @var NotifierInterface + */ + private $notifier; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var ExportManagementInterface + */ + private $exportManager; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * Consumer constructor. + * @param \Psr\Log\LoggerInterface $logger + * @param ExportManagementInterface $exportManager + * @param Filesystem $filesystem + * @param NotifierInterface $notifier + */ + public function __construct( + \Psr\Log\LoggerInterface $logger, + ExportManagementInterface $exportManager, + Filesystem $filesystem, + NotifierInterface $notifier + ) { + $this->logger = $logger; + $this->exportManager = $exportManager; + $this->filesystem = $filesystem; + $this->notifier = $notifier; + } + + /** + * Consumer logic. + * + * @param ExportInfoInterface $exportInfo + * @return void + */ + public function process(ExportInfoInterface $exportInfo) + { + try { + $data = $this->exportManager->export($exportInfo); + $fileName = $exportInfo->getFileName(); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $directory->writeFile('export/' . $fileName, $data); + + $this->notifier->addMajor( + __('Your export file is ready'), + __('You can pick up your file at export main page') + ); + } catch (LocalizedException | FileSystemException $exception) { + $this->notifier->addCritical( + __('Error during export process occurred'), + __('Error during export process occurred. Please check logs for detail') + ); + $this->logger->critical('Something went wrong while export process. ' . $exception->getMessage()); + } + } +} diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php new file mode 100644 index 0000000000000..6dffc1827cfd0 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Export\Entity; + +use \Magento\ImportExport\Api\Data\ExportInfoInterface; + +/** + * Class ExportInfo implementation for ExportInfoInterface. + */ +class ExportInfo implements ExportInfoInterface +{ + /** + * @var string + */ + private $fileFormat; + + /** + * @var string + */ + private $entity; + + /** + * @var string + */ + private $fileName; + + /** + * @var string + */ + private $contentType; + + /** + * @var mixed + */ + private $exportFilter; + + /** + * @inheritdoc + */ + public function getFileFormat() + { + return $this->fileFormat; + } + + /** + * @inheritdoc + */ + public function setFileFormat($fileFormat) + { + $this->fileFormat = $fileFormat; + } + + /** + * @inheritdoc + */ + public function getFileName() + { + return $this->fileName; + } + + /** + * @inheritdoc + */ + public function setFileName($fileName) + { + $this->fileName = $fileName; + } + + /** + * @inheritdoc + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * @inheritdoc + */ + public function setContentType($contentType) + { + $this->contentType = $contentType; + } + + /** + * @inheritdoc + */ + public function getEntity() + { + return $this->entity; + } + + /** + * @inheritdoc + */ + public function setEntity($entity) + { + $this->entity = $entity; + } + + /** + * @inheritdoc + */ + public function getExportFilter() + { + return $this->exportFilter; + } + + /** + * @inheritdoc + */ + public function setExportFilter($exportFilter) + { + $this->exportFilter = $exportFilter; + } +} diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php new file mode 100644 index 0000000000000..e3cbd162aa5af --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php @@ -0,0 +1,210 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Export\Entity; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\ImportExport\Api\Data\ExportInfoInterface; +use Magento\Framework\ObjectManagerInterface; +use \Psr\Log\LoggerInterface; +use Magento\ImportExport\Model\Export\ConfigInterface; +use Magento\ImportExport\Model\Export\Entity\Factory as EntityFactory; +use Magento\ImportExport\Model\Export\Adapter\Factory as AdapterFactory; +use Magento\ImportExport\Model\Export\AbstractEntity; + +/** + * Factory for Export Info + */ +class ExportInfoFactory +{ + /** + * Object Manager + * + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\ImportExport\Model\Export\ConfigInterface + */ + private $exportConfig; + + /** + * @var AdapterFactory + */ + private $exportAdapterFac; + + /** + * @var EntityFactory + */ + private $entityFactory; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * ExportInfoFactory constructor. + * @param ObjectManagerInterface $objectManager + * @param ConfigInterface $exportConfig + * @param Factory $entityFactory + * @param AdapterFactory $exportAdapterFac + * @param SerializerInterface $serializer + * @param LoggerInterface $logger + */ + public function __construct( + ObjectManagerInterface $objectManager, + ConfigInterface $exportConfig, + EntityFactory $entityFactory, + AdapterFactory $exportAdapterFac, + SerializerInterface $serializer, + LoggerInterface $logger + ) { + $this->objectManager = $objectManager; + $this->exportConfig = $exportConfig; + $this->entityFactory = $entityFactory; + $this->exportAdapterFac = $exportAdapterFac; + $this->serializer = $serializer; + $this->logger = $logger; + } + + /** + * Create ExportInfo object. + * + * @param string $fileFormat + * @param string $entity + * @param string $exportFilter + * @return ExportInfoInterface + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function create($fileFormat, $entity, $exportFilter) + { + $writer = $this->getWriter($fileFormat); + $entityAdapter = $this->getEntityAdapter($entity, $fileFormat, $exportFilter, $writer->getContentType()); + $fileName = $this->generateFileName($entity, $entityAdapter, $writer->getFileExtension()); + /** @var ExportInfoInterface $exportInfo */ + $exportInfo = $this->objectManager->create(ExportInfoInterface::class); + $exportInfo->setExportFilter($this->serializer->serialize($exportFilter)); + $exportInfo->setFileName($fileName); + $exportInfo->setEntity($entity); + $exportInfo->setFileFormat($fileFormat); + $exportInfo->setContentType($writer->getContentType()); + + return $exportInfo; + } + + /** + * Generate file name + * + * @param string $entity + * @param AbstractEntity $entityAdapter + * @param string $fileExtensions + * @return string + */ + private function generateFileName($entity, $entityAdapter, $fileExtensions) + { + $fileName = null; + if ($entityAdapter instanceof AbstractEntity) { + $fileName = $entityAdapter->getFileName(); + } + if (!$fileName) { + $fileName = $entity; + } + + return $fileName . '_' . date('Ymd_His') . '.' . $fileExtensions; + } + + /** + * Create instance of entity adapter and return it. + * + * @param string $entity + * @param string $fileFormat + * @param string $exportFilter + * @param string $contentType + * @return \Magento\ImportExport\Model\Export\AbstractEntity|AbstractEntity + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentType) + { + $entities = $this->exportConfig->getEntities(); + if (isset($entities[$entity])) { + try { + $entityAdapter = $this->entityFactory->create($entities[$entity]['model']); + } catch (\Exception $e) { + $this->logger->critical($e); + throw new \Magento\Framework\Exception\LocalizedException( + __('Please enter a correct entity model.') + ); + } + if (!$entityAdapter instanceof \Magento\ImportExport\Model\Export\Entity\AbstractEntity && + !$entityAdapter instanceof \Magento\ImportExport\Model\Export\AbstractEntity + ) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'The entity adapter object must be an instance of %1 or %2.', + \Magento\ImportExport\Model\Export\Entity\AbstractEntity::class, + \Magento\ImportExport\Model\Export\AbstractEntity::class + ) + ); + } + // check for entity codes integrity + if ($entity != $entityAdapter->getEntityTypeCode()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('The input entity code is not equal to entity adapter code.') + ); + } + } else { + throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity.')); + } + $entityAdapter->setParameters([ + 'fileFormat' => $fileFormat, + 'entity' => $entity, + 'exportFilter' => $exportFilter, + 'contentType' => $contentType, + ]); + return $entityAdapter; + } + + /** + * Returns writer for a file format + * + * @param string $fileFormat + * @return \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getWriter($fileFormat) + { + $fileFormats = $this->exportConfig->getFileFormats(); + if (isset($fileFormats[$fileFormat])) { + try { + $writer = $this->exportAdapterFac->create($fileFormats[$fileFormat]['model']); + } catch (\Exception $e) { + $this->logger->critical($e); + throw new \Magento\Framework\Exception\LocalizedException( + __('Please enter a correct entity model.') + ); + } + if (!$writer instanceof \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'The adapter object must be an instance of %1.', + \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter::class + ) + ); + } + } else { + throw new \Magento\Framework\Exception\LocalizedException(__('Please correct the file format.')); + } + return $writer; + } +} diff --git a/app/code/Magento/ImportExport/Model/Export/ExportManagement.php b/app/code/Magento/ImportExport/Model/Export/ExportManagement.php new file mode 100644 index 0000000000000..b4adcdd62b66d --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Export/ExportManagement.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Export; + +use Magento\Framework\EntityManager\HydratorInterface; +use Magento\ImportExport\Api\Data\ExportInfoInterface; +use Magento\ImportExport\Api\ExportManagementInterface; +use Magento\ImportExport\Model\ExportFactory; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * ExportManagementInterface implementation. + */ +class ExportManagement implements ExportManagementInterface +{ + /** + * @var ExportFactory + */ + private $exportModelFactory; + + /** + * @var HydratorInterface + */ + private $hydrator; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * ExportManagement constructor. + * @param ExportFactory $exportModelFactory + * @param HydratorInterface $hydrator + * @param SerializerInterface $serializer + */ + public function __construct( + ExportFactory $exportModelFactory, + HydratorInterface $hydrator, + SerializerInterface $serializer + ) { + $this->exportModelFactory = $exportModelFactory; + $this->hydrator = $hydrator; + $this->serializer = $serializer; + } + + /** + * Export logic implementation. + * + * @param ExportInfoInterface $exportInfo + * @return mixed|string + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function export(ExportInfoInterface $exportInfo) + { + $arrData = $this->hydrator->extract($exportInfo); + $arrData['export_filter'] = $this->serializer->unserialize($arrData['export_filter']); + /** @var \Magento\ImportExport\Model\Export $exportModel */ + $exportModel = $this->exportModelFactory->create(); + $exportModel->setData($arrData); + return $exportModel->export(); + } +} diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 1372322ee5855..2a4d6904b11b5 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -7,10 +7,12 @@ namespace Magento\ImportExport\Model; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\HTTP\Adapter\FileTransferFactory; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Framework\Message\ManagerInterface; /** * Import model @@ -124,6 +126,11 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ protected $_importExportData = null; + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $_coreConfig; + /** * @var \Magento\ImportExport\Model\Import\ConfigInterface */ @@ -179,6 +186,11 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ private $localeDate; + /** + * @var ManagerInterface + */ + private $messageManager; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Filesystem $filesystem @@ -195,6 +207,7 @@ class Import extends \Magento\ImportExport\Model\AbstractModel * @param History $importHistoryModel * @param DateTime $localeDate * @param array $data + * @param ManagerInterface|null $messageManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -212,7 +225,8 @@ public function __construct( \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\ImportExport\Model\History $importHistoryModel, DateTime $localeDate, - array $data = [] + array $data = [], + ManagerInterface $messageManager = null ) { $this->_importExportData = $importExportData; $this->_coreConfig = $coreConfig; @@ -227,6 +241,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->importHistoryModel = $importHistoryModel; $this->localeDate = $localeDate; + $this->messageManager = $messageManager ?: ObjectManager::getInstance()->get(ManagerInterface::class); parent::__construct($logger, $filesystem, $data); } @@ -302,7 +317,7 @@ public function getOperationResultMessages(ProcessingErrorAggregatorInterface $v { $messages = []; if ($this->getProcessedRowsCount()) { - if ($validationResult->getErrorsCount()) { + if ($validationResult->isErrorLimitExceeded()) { $messages[] = __('Data validation failed. Please fix the following errors and upload the file again.'); // errors info @@ -620,12 +635,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $messages = $this->getOperationResultMessages($errorAggregator); $this->addLogComment($messages); - $result = !$errorAggregator->getErrorsCount(); - $validationStrategy = $this->getData(self::FIELD_NAME_VALIDATION_STRATEGY); - if ($validationStrategy === ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS) { - $result = true; - } - + $result = !$errorAggregator->isErrorLimitExceeded(); if ($result) { $this->addLogComment(__('Import data validation is complete.')); } diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php index 1fc3257ff2c1e..10b29a50a4064 100644 --- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php @@ -408,7 +408,9 @@ protected function _saveValidatedBunches() if ($source->valid()) { try { $rowData = $source->current(); - $skuSet[$rowData['sku']] = true; + if (array_key_exists('sku', $rowData)) { + $skuSet[$rowData['sku']] = true; + } } catch (\InvalidArgumentException $e) { $this->addRowError($e->getMessage(), $this->_processedRowsCount); $this->_processedRowsCount++; @@ -436,7 +438,7 @@ protected function _saveValidatedBunches() $source->next(); } } - $this->_processedEntitiesCount = count($skuSet); + $this->_processedEntitiesCount = (count($skuSet)) ? : $this->_processedRowsCount; return $this; } diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 7f49e2022c410..028bf2c464d4b 100644 --- a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -61,6 +61,8 @@ public function __construct( } /** + * Add error via code and level + * * @param string $errorCode * @param string $errorLevel * @param int|null $rowNumber @@ -96,6 +98,8 @@ public function addError( } /** + * Add row to be skipped during import + * * @param int $rowNumber * @return $this */ @@ -110,6 +114,8 @@ public function addRowToSkip($rowNumber) } /** + * Add specific row to invalid list via row number + * * @param int $rowNumber * @return $this */ @@ -126,6 +132,8 @@ protected function processInvalidRow($rowNumber) } /** + * Add error message template + * * @param string $code * @param string $template * @return $this @@ -138,6 +146,8 @@ public function addErrorMessageTemplate($code, $template) } /** + * Check if row is invalid by row number + * * @param int $rowNumber * @return bool */ @@ -147,6 +157,8 @@ public function isRowInvalid($rowNumber) } /** + * Get number of invalid rows + * * @return int */ public function getInvalidRowsCount() @@ -155,6 +167,8 @@ public function getInvalidRowsCount() } /** + * Initialize validation strategy + * * @param string $validationStrategy * @param int $allowedErrorCount * @return $this @@ -178,6 +192,8 @@ public function initValidationStrategy($validationStrategy, $allowedErrorCount = } /** + * Check if import has to be terminated + * * @return bool */ public function hasToBeTerminated() @@ -186,15 +202,17 @@ public function hasToBeTerminated() } /** + * Check if error limit has been exceeded + * * @return bool */ public function isErrorLimitExceeded() { $isExceeded = false; - $errorsCount = $this->getErrorsCount([ProcessingError::ERROR_LEVEL_NOT_CRITICAL]); + $errorsCount = $this->getErrorsCount(); if ($errorsCount > 0 && $this->validationStrategy == self::VALIDATION_STRATEGY_STOP_ON_ERROR - && $errorsCount >= $this->allowedErrorsCount + && $errorsCount > $this->allowedErrorsCount ) { $isExceeded = true; } @@ -203,6 +221,8 @@ public function isErrorLimitExceeded() } /** + * Check if import has a fatal error + * * @return bool */ public function hasFatalExceptions() @@ -211,6 +231,8 @@ public function hasFatalExceptions() } /** + * Get all errors from an import process + * * @return ProcessingError[] */ public function getAllErrors() @@ -228,6 +250,8 @@ public function getAllErrors() } /** + * Get a specific set of errors via codes + * * @param string[] $codes * @return ProcessingError[] */ @@ -244,6 +268,8 @@ public function getErrorsByCode(array $codes) } /** + * Get an error via row number + * * @param int $rowNumber * @return ProcessingError[] */ @@ -258,6 +284,8 @@ public function getErrorByRowNumber($rowNumber) } /** + * Get a set rows via a set of error codes + * * @param array $errorCode * @param array $excludedCodes * @param bool $replaceCodeWithMessage @@ -292,6 +320,8 @@ public function getRowsGroupedByErrorCode( } /** + * Get the max allowed error count + * * @return int */ public function getAllowedErrorsCount() @@ -300,6 +330,8 @@ public function getAllowedErrorsCount() } /** + * Get current error count + * * @param string[] $errorLevels * @return int */ @@ -318,6 +350,8 @@ public function getErrorsCount( } /** + * Clear the error aggregator + * * @return $this */ public function clear() @@ -331,6 +365,8 @@ public function clear() } /** + * Check if an error has already been added to the aggregator + * * @param int $rowNum * @param string $errorCode * @param string $columnName @@ -348,6 +384,8 @@ protected function isErrorAlreadyAdded($rowNum, $errorCode, $columnName = null) } /** + * Build an error message via code, message and column name + * * @param string $errorCode * @param string $errorMessage * @param string $columnName @@ -369,6 +407,8 @@ protected function getErrorMessage($errorCode, $errorMessage, $columnName) } /** + * Process the error statistics for a given error level + * * @param string $errorLevel * @return $this */ diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml new file mode 100644 index 0000000000000..a9100b4730b8c --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml @@ -0,0 +1,30 @@ +<?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="AdminImportProductsActionGroup"> + <arguments> + <argument name="behavior" type="string"/> + <argument name="importFile" type="string"/> + <argument name="importMessage" type="string"/> + </arguments> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <waitForPageLoad stepKey="AdminImportMainSectionLoad"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="{{importFile}}" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <waitForPageLoad stepKey="AdminImportMainSectionLoad2"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="AdminMessagesSection"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="{{importMessage}}" stepKey="seeImportMessage"/> + </actionGroup> + </actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/ImportExport/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..c09cd192d05c7 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?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="AdminMenuSystemDataTransferExport"> + <data key="pageTitle">Export</data> + <data key="title">Export</data> + <data key="dataUiId">magento-importexport-system-convert-export</data> + </entity> + <entity name="AdminMenuSystemDataTransferImport"> + <data key="pageTitle">Import</data> + <data key="title">Import</data> + <data key="dataUiId">magento-importexport-system-convert-import</data> + </entity> +</entities> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml new file mode 100644 index 0000000000000..55ed3edd9bc79 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminExportIndexPage" url="admin/export/" area="admin" module="Magento_ImportExport"> + <section name="AdminExportMainSection"/> + </page> +</pages> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml new file mode 100644 index 0000000000000..528ad23aaf2bf --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml @@ -0,0 +1,21 @@ +<?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="AdminExportAttributeSection"> + <element name="filterByAttributeCode" type="input" selector="#export_filter_grid_filter_attribute_code"/> + <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> + <element name="search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="chooseAttribute" type="checkbox" selector="//*[@name='export_filter[{{var}}]']/ancestor::tr//input[@type='checkbox']" parameterized="true"/> + <element name="fillFilter" type="input" selector="//*[@name='export_filter[{{var}}]']/ancestor::tr//input[@type='text']" parameterized="true"/> + <element name="continueBtn" type="button" selector="//*[@id='export_filter_container']/button" timeout="30"/> + <element name="selectByIndex" type="button" selector="//tr[@data-repeat-index='{{var}}']//button" parameterized="true" timeout="30"/> + <element name="download" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Download']" parameterized="true" timeout="30"/> + <element name="delete" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Delete']" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportMainSection.xml new file mode 100644 index 0000000000000..da1d928607e75 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportMainSection.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="AdminExportMainSection"> + <element name="entityType" type="select" selector="#entity"/> + <element name="entityAttributes" type="select" selector="#export_filter_form"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml new file mode 100644 index 0000000000000..e8fb12ca521c2 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminExportPageNavigateMenuTest"> + <annotations> + <features value="ImportExport"/> + <stories value="Menu Navigation"/> + <title value="Admin export page navigate menu test"/> + <description value="Admin should be able to navigate to System > Export"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14157"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToExportPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemDataTransferExport.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemDataTransferExport.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml new file mode 100644 index 0000000000000..ceb4e93e4e9aa --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -0,0 +1,123 @@ +<?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="AdminImportProductsWithAddUpdateBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with add/update behavior."/> + <stories value="Import Products"/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with add/update behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14077"/> + <group value="importExport"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product1 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <field key="name">SimpleProductForTest1</field> + <field key="sku">SimpleProductForTest1</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create Website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + + <!-- Create store group --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> + <!-- Delete all products that replaced products in the before block post import --> + <deleteData stepKey="deleteSimpleProduct1" url="/V1/products/SimpleProductForTest1"/> + <deleteData stepKey="deleteSimpleProduct2" url="/V1/products/SimpleProductForTest2"/> + <deleteData stepKey="deleteSimpleProduct3" url="/V1/products/SimpleProductForTest3"/> + + <!-- Delete category created in the before block --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Delete website created in the before block --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite" before="logoutFromAdmin"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="catalog_import_products.csv"/> + <argument name="importMessage" value="Created: 2, Updated: 1, Deleted: 0"/> + </actionGroup> + + <!-- Assert Simple Product1 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct1OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert Simple Product1 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct1OnEditPage"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert Simple Product2 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct2OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert Simple Product2 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct2OnEditPage"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert Simple Product3 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct3OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + + <!-- Assert Simple Product3 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct3OnEditPage"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + + <!-- Assert SimpleProduct1 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct1Validation"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert SimpleProduct2 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct2Validation"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert SimpleProduct3 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct3Validation"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml new file mode 100644 index 0000000000000..d63a5546716b1 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.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="AdminImportProductsWithReplaceBehaviorTest" extends="AdminImportProductsWithAddUpdateBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with replace behavior."/> + <stories value="Import Products"/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with replace behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14076"/> + <group value="importExport"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product2 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <field key="name">SimpleProductForTest2</field> + <field key="sku">SimpleProductForTest2</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Simple Product3 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct3"> + <field key="name">SimpleProductForTest3</field> + <field key="sku">SimpleProductForTest3</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <!-- Import products with replace behavior --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Replace"/> + <argument name="importFile" value="catalog_import_products.csv"/> + <argument name="importMessage" value="Created: 3, Updated: 0, Deleted: 3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml new file mode 100644 index 0000000000000..9913933d857a8 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemImportNavigateMenuTest"> + <annotations> + <features value="ImportExport"/> + <stories value="Menu Navigation"/> + <title value="Admin system import navigate menu test"/> + <description value="Admin should be able to navigate to System > Import"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14156"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToImportPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemDataTransferImport.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemDataTransferImport.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php index 288a99770974a..179f3f3cadab0 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php @@ -22,15 +22,15 @@ 'attributes_with_type_modelName_and_invalid_value' => [ '<?xml version="1.0"?><config><entity name="Name/one" model="model_one" ' . 'entityAttributeFilterType="model_one"/><entityType entity="Name/one" name="name_one" model="1"/>' - . ' <fileFormat name="name_one" model="model1"/></config>', + . ' <fileFormat name="name_one" model="1model"/></config>', [ "Element 'entityType', attribute 'model': [facet 'pattern'] The value '1' is not accepted by the " . - "pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entityType', attribute 'model': '1' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n", - "Element 'fileFormat', attribute 'model': [facet 'pattern'] The value 'model1' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", - "Element 'fileFormat', attribute 'model': 'model1' is not a valid " . + "Element 'fileFormat', attribute 'model': [facet 'pattern'] The value '1model' is not " . + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'fileFormat', attribute 'model': '1model' is not a valid " . "value of the atomic type 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php index 357b35e8a313c..409c1af9cb38a 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php @@ -26,12 +26,12 @@ ["Element 'entity', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], ], 'entity_model_with_invalid_value' => [ - '<?xml version="1.0"?><config><entity name="test_name" label="test_label" model="afwer34" ' . + '<?xml version="1.0"?><config><entity name="test_name" label="test_label" model="34afwer" ' . 'behaviorModel="test" /></config>', [ - "Element 'entity', attribute 'model': [facet 'pattern'] The value 'afwer34' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", - "Element 'entity', attribute 'model': 'afwer34' is not a valid value of the atomic type" . + "Element 'entity', attribute 'model': [facet 'pattern'] The value '34afwer' is not " . + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'entity', attribute 'model': '34afwer' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], ], @@ -40,7 +40,7 @@ '</config>', [ "Element 'entity', attribute 'behaviorModel': [facet 'pattern'] The value '666' is not accepted by " . - "the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entity', attribute 'behaviorModel': '666' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php index c913b53e8b531..c7b06a8731f02 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php @@ -19,7 +19,7 @@ '<?xml version="1.0"?><config><entity name="some_name" model="12345"/></config>', [ "Element 'entity', attribute 'model': [facet 'pattern'] The value '12345' is not accepted by " . - "the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entity', attribute 'model': '12345' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -28,7 +28,7 @@ '<?xml version="1.0"?><config><entity name="some_name" behaviorModel="=--09"/></config>', [ "Element 'entity', attribute 'behaviorModel': [facet 'pattern'] The value '=--09' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entity', attribute 'behaviorModel': '=--09' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -46,11 +46,11 @@ ["Element 'entityType': The attribute 'model' is required but missing.\nLine: 1\n"], ], 'entitytype_with_invalid_model_attribute_value' => [ - '<?xml version="1.0"?><config><entityType entity="entity_name" name="some_name" model="test1"/></config>', + '<?xml version="1.0"?><config><entityType entity="entity_name" name="some_name" model="1test"/></config>', [ - "Element 'entityType', attribute 'model': [facet 'pattern'] The value 'test1' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", - "Element 'entityType', attribute 'model': 'test1' is not a valid value of the atomic type" . + "Element 'entityType', attribute 'model': [facet 'pattern'] The value '1test' is not " . + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'entityType', attribute 'model': '1test' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php index b81b9f9093d1f..722cca4af6d49 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/ErrorProcessing/ProcessingErrorAggregatorTest.php @@ -216,6 +216,7 @@ public function testIsErrorLimitExceededTrue() */ public function testIsErrorLimitExceededFalse() { + $this->model->initValidationStrategy('validation-stop-on-errors', 5); $this->model->addError('systemException'); $this->model->addError('systemException', 'critical', 7, 'Some column name', 'Message', 'Description'); $this->model->addError('systemException', 'critical', 4, 'Some column name', 'Message', 'Description'); diff --git a/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php b/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php new file mode 100644 index 0000000000000..a7b9b072f00f4 --- /dev/null +++ b/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Ui\Component\Columns; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\ImportExport\Controller\Adminhtml\Export\File\Download; +use Magento\ImportExport\Controller\Adminhtml\Export\File\Delete; +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\UrlInterface; + +/** + * Actions for export grid. + */ +class ExportGridActions extends Column +{ + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * ExportGridActions constructor. + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param UrlInterface $urlBuilder + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + UrlInterface $urlBuilder, + array $components = [], + array $data = [] + ) { + $this->urlBuilder = $urlBuilder; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + $name = $this->getData('name'); + if (isset($item['file_name'])) { + $item[$name]['view'] = [ + 'href' => $this->urlBuilder->getUrl(Download::URL, ['filename' => $item['file_name']]), + 'label' => __('Download') + ]; + $item[$name]['delete'] = [ + 'href' => $this->urlBuilder->getUrl(Delete::URL, ['filename' => $item['file_name']]), + 'label' => __('Delete'), + 'confirm' => [ + 'title' => __('Delete'), + 'message' => __('Are you sure you wan\'t to delete a file?') + ] + ]; + } + } + } + return $dataSource; + } +} diff --git a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php new file mode 100644 index 0000000000000..e64a6df430ea1 --- /dev/null +++ b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Ui\DataProvider; + +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; + +/** + * Data provider for export grid. + */ +class ExportFileDataProvider extends DataProvider +{ + /** + * @var DriverInterface + */ + private $file; + + /** + * @var Filesystem + */ + private $fileSystem; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param \Magento\Framework\Api\Search\ReportingInterface $reporting + * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\App\RequestInterface $request + * @param \Magento\Framework\Api\FilterBuilder $filterBuilder + * @param DriverInterface $file + * @param Filesystem $filesystem + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $name, + string $primaryFieldName, + string $requestFieldName, + \Magento\Framework\Api\Search\ReportingInterface $reporting, + \Magento\Framework\Api\Search\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\App\RequestInterface $request, + \Magento\Framework\Api\FilterBuilder $filterBuilder, + DriverInterface $file, + Filesystem $filesystem, + array $meta = [], + array $data = [] + ) { + $this->file = $file; + $this->fileSystem = $filesystem; + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + } + + /** + * Returns data for grid. + * + * @return array + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function getData() + { + $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); + $emptyResponse = ['items' => [], 'totalRecords' => 0]; + if (!$this->file->isExists($directory->getAbsolutePath() . 'export/')) { + return $emptyResponse; + } + + $files = $this->file->readDirectoryRecursively($directory->getAbsolutePath() . 'export/'); + if (empty($files)) { + return $emptyResponse; + } + $result = []; + foreach ($files as $file) { + $result['items'][]['file_name'] = basename($file); + } + + $result['totalRecords'] = count($result['items']); + + return $result; + } +} diff --git a/app/code/Magento/ImportExport/composer.json b/app/code/Magento/ImportExport/composer.json index b0ba04f5aa0eb..6363722eba7c8 100644 --- a/app/code/Magento/ImportExport/composer.json +++ b/app/code/Magento/ImportExport/composer.json @@ -12,7 +12,8 @@ "magento/module-catalog": "*", "magento/module-eav": "*", "magento/module-media-storage": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml index 03c24c7b2bf69..04ee726349123 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -6,9 +6,24 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="\Magento\ImportExport\Controller\Adminhtml\Import\Start"> + <type name="Magento\ImportExport\Controller\Adminhtml\Import\Start"> <arguments> <argument name="exceptionMessageFactory" xsi:type="object">Magento\Framework\Message\ExceptionMessageLookupFactory</argument> </arguments> </type> + <type name="Magento\ImportExport\Model\Export\Entity\ExportInfoFactory"> + <arguments> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Controller\Adminhtml\Export\File\Delete"> + <arguments> + <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider"> + <arguments> + <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/etc/communication.xml b/app/code/Magento/ImportExport/etc/communication.xml new file mode 100644 index 0000000000000..7794b3e5ab248 --- /dev/null +++ b/app/code/Magento/ImportExport/etc/communication.xml @@ -0,0 +1,12 @@ +<?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:Communication/etc/communication.xsd"> + <topic name="import_export.export" request="Magento\ImportExport\Api\Data\ExportInfoInterface"> + <handler name="exportProcessor" type="Magento\ImportExport\Model\Export\Consumer" method="process" /> + </topic> +</config> diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index 36c76022a41c7..909b526e4790c 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -10,6 +10,8 @@ <preference for="Magento\ImportExport\Model\Export\ConfigInterface" type="Magento\ImportExport\Model\Export\Config" /> <preference for="Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface" type="Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator" /> <preference for="Magento\ImportExport\Model\Report\ReportProcessorInterface" type="Magento\ImportExport\Model\Report\Csv" /> + <preference for="Magento\ImportExport\Api\Data\ExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> + <preference for="Magento\ImportExport\Api\ExportManagementInterface" type="Magento\ImportExport\Model\Export\ExportManagement" /> <type name="Magento\Framework\Module\Setup\Migration"> <arguments> <argument name="compositeModules" xsi:type="array"> diff --git a/app/code/Magento/ImportExport/etc/export.xsd b/app/code/Magento/ImportExport/etc/export.xsd index 65728a9be5c62..f62dbc891ef0f 100644 --- a/app/code/Magento/ImportExport/etc/export.xsd +++ b/app/code/Magento/ImportExport/etc/export.xsd @@ -71,11 +71,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [A-Za-z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[A-Za-z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/ImportExport/etc/import.xsd b/app/code/Magento/ImportExport/etc/import.xsd index aefa6541d7e13..e73038ebc0710 100644 --- a/app/code/Magento/ImportExport/etc/import.xsd +++ b/app/code/Magento/ImportExport/etc/import.xsd @@ -61,11 +61,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [A-Za-z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[A-Za-z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/ImportExport/etc/queue.xml b/app/code/Magento/ImportExport/etc/queue.xml new file mode 100644 index 0000000000000..7eb96819faf10 --- /dev/null +++ b/app/code/Magento/ImportExport/etc/queue.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/queue.xsd"> + <broker topic="import_export.export" exchange="magento-db" type="db"> + <queue name="export" consumer="exportProcessor" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\ImportExport\Model\Export\Consumer::process"/> + </broker> +</config> diff --git a/app/code/Magento/ImportExport/etc/queue_consumer.xml b/app/code/Magento/ImportExport/etc/queue_consumer.xml new file mode 100644 index 0000000000000..2c6612ac0ef1c --- /dev/null +++ b/app/code/Magento/ImportExport/etc/queue_consumer.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-message-queue:etc/consumer.xsd"> + <consumer name="exportProcessor" queue="export" connection="db" maxMessages="100" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\ImportExport\Model\Export\Consumer::process" /> +</config> diff --git a/app/code/Magento/ImportExport/etc/queue_publisher.xml b/app/code/Magento/ImportExport/etc/queue_publisher.xml new file mode 100644 index 0000000000000..097b60bee1534 --- /dev/null +++ b/app/code/Magento/ImportExport/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/publisher.xsd"> + <publisher topic="import_export.export"> + <connection name="db" exchange="magento-db" /> + </publisher> +</config> diff --git a/app/code/Magento/ImportExport/etc/queue_topology.xml b/app/code/Magento/ImportExport/etc/queue_topology.xml new file mode 100644 index 0000000000000..f77c13e2ba05f --- /dev/null +++ b/app/code/Magento/ImportExport/etc/queue_topology.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="exportBinding" topic="import_export.export" destinationType="queue" destination="export"/> + </exchange> +</config> diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index cae4d6e19868d..d7680a71ac5f7 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -18,6 +18,7 @@ Import,Import "Import Settings","Import Settings" "Import Behavior","Import Behavior" " "," " +"Validation Strategy","Validation Strategy" "Stop on Error","Stop on Error" "Skip error entries","Skip error entries" "Allowed Errors Count","Allowed Errors Count" diff --git a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_export_index.xml b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_export_index.xml index 6848650979306..b60fb40bfbd83 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_export_index.xml +++ b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_export_index.xml @@ -11,6 +11,7 @@ <block class="Magento\Backend\Block\Template" template="Magento_ImportExport::export/form/before.phtml" name="export.form.before" as="form_before"/> <block class="Magento\ImportExport\Block\Adminhtml\Export\Edit" name="export.form.container"/> <block class="Magento\ImportExport\Block\Adminhtml\Form\After" template="Magento_ImportExport::export/form/after.phtml" name="export.form.after" as="form_after"/> + <uiComponent name="export_grid"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml b/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml new file mode 100644 index 0000000000000..2b160bc9f6f40 --- /dev/null +++ b/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">export_grid.export_grid_data_source</item> + </item> + </argument> + <settings> + <deps> + <dep>export_grid.export_grid_data_source</dep> + </deps> + <spinner>export_grid_columns</spinner> + </settings> + <dataSource name="export_grid_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">file_name</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_ImportExport::export</aclResource> + <dataProvider class="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider" name="export_grid_data_source"> + <settings> + <requestFieldName>file_name</requestFieldName> + <primaryFieldName>file_name</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <columns name="export_grid_columns"> + <column name="file_name"> + <settings> + <sortable>false</sortable> + <label translate="true">File name</label> + </settings> + </column> + <actionsColumn name="actions" class="Magento\ImportExport\Ui\Component\Columns\ExportGridActions"> + <settings> + <indexField>file_name</indexField> + </settings> + </actionsColumn> + </columns> +</listing> \ No newline at end of file diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 87a7cce58e1a5..2821a46f29416 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Indexer\Model; use Magento\Framework\Indexer\ActionFactory; @@ -14,6 +15,8 @@ use Magento\Framework\Indexer\StructureFactory; /** + * Indexer model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface @@ -361,7 +364,7 @@ public function getLatestUpdated() return $this->getView()->getUpdated(); } } - return $this->getState()->getUpdated(); + return $this->getState()->getUpdated() ?: ''; } /** diff --git a/app/code/Magento/Indexer/Model/ProcessManager.php b/app/code/Magento/Indexer/Model/ProcessManager.php index 04cd713fffb11..2f2c500e028cf 100644 --- a/app/code/Magento/Indexer/Model/ProcessManager.php +++ b/app/code/Magento/Indexer/Model/ProcessManager.php @@ -71,6 +71,7 @@ public function execute($userFunctions) private function simpleThreadExecute($userFunctions) { foreach ($userFunctions as $userFunction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func($userFunction); } } @@ -79,6 +80,7 @@ private function simpleThreadExecute($userFunctions) * Execute user functions in multiThreads mode * * @param \Traversable $userFunctions + * @throws \RuntimeException * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ private function multiThreadsExecute($userFunctions) @@ -86,6 +88,7 @@ private function multiThreadsExecute($userFunctions) $this->resource->closeConnection(null); $threadNumber = 0; foreach ($userFunctions as $userFunction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $pid = pcntl_fork(); if ($pid == -1) { throw new \RuntimeException('Unable to fork a new process'); @@ -95,6 +98,7 @@ private function multiThreadsExecute($userFunctions) $this->startChildProcess($userFunction); } } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock,Magento2.Functions.DiscouragedFunction while (pcntl_waitpid(0, $status) != -1) { //Waiting for the completion of child processes } @@ -128,12 +132,13 @@ private function isSetupMode(): bool * Start child process * * @param callable $userFunction - * @SuppressWarnings(PHPMD.ExitExpression) */ private function startChildProcess(callable $userFunction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $status = call_user_func($userFunction); - $status = is_integer($status) ? $status : 0; + $status = is_int($status) ? $status : 0; + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit($status); } @@ -146,8 +151,10 @@ private function executeParentProcess(int &$threadNumber) { $threadNumber++; if ($threadNumber >= $this->threadsCount) { + // phpcs:disable Magento2.Functions.DiscouragedFunction pcntl_wait($status); if (pcntl_wexitstatus($status) !== 0) { + // phpcs:enable $this->failInChildProcess = true; } $threadNumber--; diff --git a/app/code/Magento/Indexer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Indexer/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..b74d521f2cb36 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Data/AdminMenuData.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="AdminMenuSystemToolsIndexManagement"> + <data key="pageTitle">Index Management</data> + <data key="title">Index Management</data> + <data key="dataUiId">magento-indexer-system-index</data> + </entity> +</entities> diff --git a/app/code/Magento/Indexer/Test/Mftf/Page/AdminIndexManagementPage.xml b/app/code/Magento/Indexer/Test/Mftf/Page/AdminIndexManagementPage.xml new file mode 100644 index 0000000000000..ed9a3dbb8c74b --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Page/AdminIndexManagementPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminIndexManagementPage" url="indexer/indexer/list/" area="admin" module="Indexer"> + <section name="AdminIndexManagementSection"/> + </page> +</pages> diff --git a/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml b/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml index db98116c224dd..860b600de2b53 100644 --- a/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml +++ b/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml @@ -9,9 +9,12 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminIndexManagementSection"> - <!--<element name="catalogSearchCheckbox" type="checkbox" selector="input[value='catalogsearch_fulltext']"/>--> + <element name="catalogSearchCheckbox" type="checkbox" selector="input[value='catalogsearch_fulltext']"/> <element name="indexerCheckbox" type="checkbox" selector="input[value='{{var1}}']" parameterized="true"/> <element name="massActionSelect" type="select" selector="#gridIndexer_massaction-select"/> <element name="massActionSubmit" type="button" selector="#gridIndexer_massaction-form button"/> + <element name="indexerSelect" type="select" selector="//select[contains(@class,'action-select-multiselect')]"/> + <element name="indexerStatus" type="text" selector="//tr[descendant::td[contains(., '{{status}}')]]//*[contains(@class, 'col-indexer_status')]/span" parameterized="true"/> + <element name="successMessage" type="text" selector="//*[@data-ui-id='messages-message-success']"/> </section> </sections> diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml new file mode 100644 index 0000000000000..140c93f7f2b48 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemIndexManagementNavigateMenuTest"> + <annotations> + <features value="Indexer"/> + <stories value="Menu Navigation"/> + <title value="Admin system index management navigate menu test"/> + <description value="Admin should be able to navigate to System > Index Management"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14127"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToIndexManagementPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemToolsIndexManagement.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemToolsIndexManagement.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 6b7cc12218990..ca2da9585f934 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -164,7 +164,12 @@ public function testGetLatestUpdated($getViewIsEnabled, $getViewGetUpdated, $get } } } else { - $this->assertEquals($getStateGetUpdated, $this->model->getLatestUpdated()); + $getLatestUpdated = $this->model->getLatestUpdated(); + $this->assertEquals($getStateGetUpdated, $getLatestUpdated); + + if ($getStateGetUpdated === null) { + $this->assertNotNull($getLatestUpdated); + } } } @@ -182,7 +187,8 @@ public function getLatestUpdatedDataProvider() [true, '', '06-Jan-1944'], [true, '06-Jan-1944', ''], [true, '', ''], - [true, '06-Jan-1944', '05-Jan-1944'] + [true, '06-Jan-1944', '05-Jan-1944'], + [false, null, null], ]; } diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml index c7603191e8606..76e7e7a46224b 100644 --- a/app/code/Magento/Indexer/etc/di.xml +++ b/app/code/Magento/Indexer/etc/di.xml @@ -42,7 +42,7 @@ <plugin name="page-cache-indexer-reindex-clean-cache" type="Magento\Indexer\Model\Processor\CleanCache" sortOrder="10"/> </type> - <type name="\Magento\Indexer\Model\ProcessManager"> + <type name="Magento\Indexer\Model\ProcessManager"> <arguments> <argument name="threadsCount" xsi:type="init_parameter">Magento\Indexer\Model\ProcessManager::THREADS_COUNT</argument> </arguments> diff --git a/app/code/Magento/Integration/Model/AdminTokenService.php b/app/code/Magento/Integration/Model/AdminTokenService.php index 5a030325e9fbd..7726ff979c6d7 100644 --- a/app/code/Magento/Integration/Model/AdminTokenService.php +++ b/app/code/Magento/Integration/Model/AdminTokenService.php @@ -72,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function createAdminAccessToken($username, $password) { @@ -110,7 +110,7 @@ public function revokeAdminAccessToken($adminId) { $tokenCollection = $this->tokenModelCollectionFactory->create()->addFilterByAdminId($adminId); if ($tokenCollection->getSize() == 0) { - throw new LocalizedException(__('This user has no tokens.')); + return true; } try { foreach ($tokenCollection as $token) { diff --git a/app/code/Magento/Integration/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Integration/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..be5b3f16ad3c5 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Data/AdminMenuData.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="AdminMenuSystemExtensionsIntegrations"> + <data key="pageTitle">Integrations</data> + <data key="title">Integrations</data> + <data key="dataUiId">magento-integration-system-integrations</data> + </entity> +</entities> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml new file mode 100644 index 0000000000000..3bd149d222c0e --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemIntegrationsNavigateMenuTest"> + <annotations> + <features value="Integration"/> + <stories value="Menu Navigation"/> + <title value="Admin system integrations navigate menu test"/> + <description value="Admin should be able to navigate to System > Integrations"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14149"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToIntegrationsPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemExtensionsIntegrations.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemExtensionsIntegrations.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php b/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php index e0b1113e80d8d..83efe4074e15f 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php @@ -8,6 +8,9 @@ use Magento\Integration\Model\Oauth\Token; +/** + * Test for Magento\Integration\Model\AdminTokenService class. + */ class AdminTokenServiceTest extends \PHPUnit\Framework\TestCase { /** \Magento\Integration\Model\AdminTokenService */ @@ -99,10 +102,6 @@ public function testRevokeAdminAccessToken() $this->assertTrue($this->_tokenService->revokeAdminAccessToken($adminId)); } - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage This user has no tokens. - */ public function testRevokeAdminAccessTokenWithoutAdminId() { $this->_tokenModelCollectionMock->expects($this->once()) diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml index ad94d44d636e9..b44ee9ddbd734 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml @@ -16,4 +16,8 @@ <element name="PriceNavigationStep" type="button" selector="#catalog_layered_navigation_price_range_step"/> <element name="PriceNavigationStepSystemValue" type="button" selector="#catalog_layered_navigation_price_range_step_inherit"/> </section> + + <section name="StorefrontLayeredNavigationSection"> + <element name="shoppingOptionsByName" type="button" selector="//*[text()='Shopping Options']/..//*[contains(text(),'{{arg}}')]" parameterized="true"/> + </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml b/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml index de4637847456e..8d3f70c2806aa 100644 --- a/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml +++ b/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml @@ -20,7 +20,7 @@ </field> <field id="price_range_step" translate="label" type="text" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Default Price Navigation Step</label> - <validate>validate-number validate-number-range number-range-0.01-1000000000</validate> + <validate>validate-number validate-number-range number-range-0.01-9999999999999999</validate> <depends> <field id="price_range_calculation">manual</field> </depends> diff --git a/app/code/Magento/Marketplace/Block/Partners.php b/app/code/Magento/Marketplace/Block/Partners.php index 4f8ca798f1756..30d6a2910f4de 100644 --- a/app/code/Magento/Marketplace/Block/Partners.php +++ b/app/code/Magento/Marketplace/Block/Partners.php @@ -7,6 +7,8 @@ namespace Magento\Marketplace\Block; /** + * Partners section block. + * * @api * @since 100.0.2 */ @@ -39,7 +41,7 @@ public function __construct( /** * Gets partners * - * @return bool|string + * @return array */ public function getPartners() { diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index 6e3929296e252..aae90512b3d95 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -24,6 +24,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Image resize service. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ImageResize @@ -123,7 +125,8 @@ public function __construct( } /** - * Create resized images of different sizes from an original image + * Create resized images of different sizes from an original image. + * * @param string $originalImageName * @throws NotFoundException */ @@ -141,7 +144,8 @@ public function resizeFromImageName(string $originalImageName) } /** - * Create resized images of different sizes from themes + * Create resized images of different sizes from themes. + * * @param array|null $themes * @return \Generator * @throws NotFoundException @@ -169,7 +173,8 @@ public function resizeFromThemes(array $themes = null): \Generator } /** - * Search the current theme + * Search the current theme. + * * @return array */ private function getThemesInUse(): array @@ -187,7 +192,8 @@ private function getThemesInUse(): array } /** - * Get view images data from themes + * Get view images data from themes. + * * @param array $themes * @return array */ @@ -211,7 +217,8 @@ private function getViewImages(array $themes): array } /** - * Get unique image index + * Get unique image index. + * * @param array $imageData * @return string */ @@ -223,7 +230,8 @@ private function getUniqueImageIndex(array $imageData): string } /** - * Make image + * Make image. + * * @param string $originalImagePath * @param array $imageParams * @return Image @@ -241,7 +249,8 @@ private function makeImage(string $originalImagePath, array $imageParams): Image } /** - * Resize image + * Resize image. + * * @param array $viewImage * @param string $originalImagePath * @param string $originalImageName @@ -257,9 +266,41 @@ private function resize(array $viewImage, string $originalImagePath, string $ori ] ); + if (isset($imageParams['watermark_file'])) { + if ($imageParams['watermark_height'] !== null) { + $image->setWatermarkHeight($imageParams['watermark_height']); + } + + if ($imageParams['watermark_width'] !== null) { + $image->setWatermarkWidth($imageParams['watermark_width']); + } + + if ($imageParams['watermark_position'] !== null) { + $image->setWatermarkPosition($imageParams['watermark_position']); + } + + if ($imageParams['watermark_image_opacity'] !== null) { + $image->setWatermarkImageOpacity($imageParams['watermark_image_opacity']); + } + + $image->watermark($this->getWatermarkFilePath($imageParams['watermark_file'])); + } + if ($imageParams['image_width'] !== null && $imageParams['image_height'] !== null) { $image->resize($imageParams['image_width'], $imageParams['image_height']); } $image->save($imageAsset->getPath()); } + + /** + * Returns watermark file absolute path + * + * @param string $file + * @return string + */ + private function getWatermarkFilePath($file) + { + $path = $this->imageConfig->getMediaPath('/watermark/' . $file); + return $this->mediaDirectory->getAbsolutePath($path); + } } diff --git a/app/code/Magento/MessageQueue/Api/PoisonPillCompareInterface.php b/app/code/Magento/MessageQueue/Api/PoisonPillCompareInterface.php new file mode 100644 index 0000000000000..3d5b895575597 --- /dev/null +++ b/app/code/Magento/MessageQueue/Api/PoisonPillCompareInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Api; + +/** + * Interface describes how to describes how to compare poison pill with latest in DB. + * + * @api + */ +interface PoisonPillCompareInterface +{ + /** + * Check if version of current poison pill is latest. + * + * @param int $poisonPillVersion + * @return bool + */ + public function isLatestVersion(int $poisonPillVersion): bool; +} diff --git a/app/code/Magento/MessageQueue/Api/PoisonPillPutInterface.php b/app/code/Magento/MessageQueue/Api/PoisonPillPutInterface.php new file mode 100644 index 0000000000000..02293c99bb3f4 --- /dev/null +++ b/app/code/Magento/MessageQueue/Api/PoisonPillPutInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Api; + +/** + * Command interface describes how to create new version on poison pill. + * + * @api + */ +interface PoisonPillPutInterface +{ + /** + * Put new version of poison pill inside DB. + * + * @return int + * @throws \Exception + */ + public function put(): int; +} diff --git a/app/code/Magento/MessageQueue/Api/PoisonPillReadInterface.php b/app/code/Magento/MessageQueue/Api/PoisonPillReadInterface.php new file mode 100644 index 0000000000000..db97990ebbad5 --- /dev/null +++ b/app/code/Magento/MessageQueue/Api/PoisonPillReadInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Api; + +/** + * Describes how to get latest version of poison pill. + * + * @api + */ +interface PoisonPillReadInterface +{ + /** + * Returns get latest version of poison pill. + * + * @return int + */ + public function getLatestVersion(): int; +} diff --git a/app/code/Magento/MessageQueue/Model/CallbackInvoker.php b/app/code/Magento/MessageQueue/Model/CallbackInvoker.php new file mode 100644 index 0000000000000..f6305363fc1a6 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/CallbackInvoker.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\Framework\MessageQueue\CallbackInvokerInterface; +use Magento\Framework\MessageQueue\QueueInterface; +use Magento\MessageQueue\Api\PoisonPillCompareInterface; +use Magento\MessageQueue\Api\PoisonPillReadInterface; + +/** + * Callback invoker + */ +class CallbackInvoker implements CallbackInvokerInterface +{ + /** + * @var PoisonPillReadInterface $poisonPillRead + */ + private $poisonPillRead; + + /** + * @var int $poisonPillVersion + */ + private $poisonPillVersion; + + /** + * @var PoisonPillCompareInterface + */ + private $poisonPillCompare; + + /** + * @param PoisonPillReadInterface $poisonPillRead + * @param PoisonPillCompareInterface $poisonPillCompare + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead, + PoisonPillCompareInterface $poisonPillCompare + ) { + $this->poisonPillRead = $poisonPillRead; + $this->poisonPillCompare = $poisonPillCompare; + } + + /** + * @inheritdoc + */ + public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback) + { + $this->poisonPillVersion = $this->poisonPillRead->getLatestVersion(); + for ($i = $maxNumberOfMessages; $i > 0; $i--) { + do { + $message = $queue->dequeue(); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + } while ($message === null && (sleep(1) === 0)); + if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) { + $queue->reject($message); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit(0); + } + $callback($message); + } + } +} diff --git a/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php b/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php new file mode 100644 index 0000000000000..a8e40ea495002 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\MessageQueue\Api\PoisonPillCompareInterface; +use Magento\MessageQueue\Api\PoisonPillReadInterface; + +/** + * Poison pill compare + */ +class PoisonPillCompare implements PoisonPillCompareInterface +{ + /** + * @var PoisonPillReadInterface + */ + private $poisonPillRead; + + /** + * PoisonPillCompare constructor. + * @param PoisonPillReadInterface $poisonPillRead + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead + ) { + $this->poisonPillRead = $poisonPillRead; + } + + /** + * @inheritdoc + */ + public function isLatestVersion(int $poisonPillVersion): bool + { + return $poisonPillVersion === $this->poisonPillRead->getLatestVersion(); + } +} diff --git a/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php b/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php new file mode 100644 index 0000000000000..283fff8ace7c7 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model\ResourceModel; + +use Magento\MessageQueue\Api\PoisonPillReadInterface; +use Magento\MessageQueue\Api\PoisonPillPutInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; + +/** + * PoisonPill. + */ +class PoisonPill extends AbstractDb implements PoisonPillPutInterface, PoisonPillReadInterface +{ + /** + * Table name. + */ + const QUEUE_POISON_PILL_TABLE = 'queue_poison_pill'; + + /** + * PoisonPill constructor. + * + * @param Context $context + * @param string|null $connectionName + */ + public function __construct( + Context $context, + string $connectionName = null + ) { + parent::__construct($context, $connectionName); + } + + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init(self::QUEUE_POISON_PILL_TABLE, 'version'); + } + + /** + * @inheritdoc + */ + public function put(): int + { + $connection = $this->getConnection(); + $table = $this->getMainTable(); + $connection->insert($table, []); + return (int)$connection->lastInsertId($table); + } + + /** + * @inheritdoc + */ + public function getLatestVersion() : int + { + $select = $this->getConnection()->select()->from( + $this->getTable(self::QUEUE_POISON_PILL_TABLE), + 'version' + )->order( + 'version ' . \Magento\Framework\DB\Select::SQL_DESC + )->limit( + 1 + ); + + $version = (int)$this->getConnection()->fetchOne($select); + + return $version; + } +} diff --git a/app/code/Magento/MessageQueue/etc/db_schema.xml b/app/code/Magento/MessageQueue/etc/db_schema.xml index 7a20d2bd4df5d..9cdf414dd06e1 100644 --- a/app/code/Magento/MessageQueue/etc/db_schema.xml +++ b/app/code/Magento/MessageQueue/etc/db_schema.xml @@ -21,4 +21,12 @@ <column name="message_code"/> </constraint> </table> + <table name="queue_poison_pill" resource="default" engine="innodb" + comment="Sequence table for poison pill versions"> + <column xsi:type="int" name="version" padding="10" unsigned="true" nullable="false" identity="true" + comment="Poison Pill version."/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="version"/> + </constraint> + </table> </schema> diff --git a/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json b/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json index f31981d2ec40f..d9d623a994b37 100644 --- a/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json +++ b/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json @@ -9,5 +9,13 @@ "PRIMARY": true, "QUEUE_LOCK_MESSAGE_CODE": true } + }, + "queue_poison_pill": { + "column": { + "version": true + }, + "constraint": { + "PRIMARY": true + } } -} \ No newline at end of file +} diff --git a/app/code/Magento/MessageQueue/etc/di.xml b/app/code/Magento/MessageQueue/etc/di.xml index c8f2edb862613..22cfea976a722 100644 --- a/app/code/Magento/MessageQueue/etc/di.xml +++ b/app/code/Magento/MessageQueue/etc/di.xml @@ -13,6 +13,10 @@ <preference for="Magento\Framework\MessageQueue\EnvelopeInterface" type="Magento\Framework\MessageQueue\Envelope"/> <preference for="Magento\Framework\MessageQueue\ConsumerInterface" type="Magento\Framework\MessageQueue\Consumer"/> <preference for="Magento\Framework\MessageQueue\MergedMessageInterface" type="Magento\Framework\MessageQueue\MergedMessage"/> + <preference for="Magento\MessageQueue\Api\PoisonPillCompareInterface" type="Magento\MessageQueue\Model\PoisonPillCompare"/> + <preference for="Magento\MessageQueue\Api\PoisonPillPutInterface" type="Magento\MessageQueue\Model\ResourceModel\PoisonPill"/> + <preference for="Magento\MessageQueue\Api\PoisonPillReadInterface" type="Magento\MessageQueue\Model\ResourceModel\PoisonPill"/> + <preference for="Magento\Framework\MessageQueue\CallbackInvokerInterface" type="Magento\MessageQueue\Model\CallbackInvoker"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/Msrp/Helper/Data.php b/app/code/Magento/Msrp/Helper/Data.php index b4ec34ebee19c..2f6dd2da9bbc4 100644 --- a/app/code/Magento/Msrp/Helper/Data.php +++ b/app/code/Magento/Msrp/Helper/Data.php @@ -7,13 +7,17 @@ use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\ObjectManager; use Magento\Msrp\Model\Product\Attribute\Source\Type; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Api\ProductRepositoryInterface; /** * Msrp data helper + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Data extends AbstractHelper { @@ -42,6 +46,11 @@ class Data extends AbstractHelper */ protected $productRepository; + /** + * @var MsrpPriceCalculatorInterface + */ + private $msrpPriceCalculator; + /** * @param Context $context * @param StoreManagerInterface $storeManager @@ -50,6 +59,7 @@ class Data extends AbstractHelper * @param \Magento\Msrp\Model\Config $config * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency * @param ProductRepositoryInterface $productRepository + * @param MsrpPriceCalculatorInterface|null $msrpPriceCalculator */ public function __construct( Context $context, @@ -58,7 +68,8 @@ public function __construct( \Magento\Msrp\Model\Msrp $msrp, \Magento\Msrp\Model\Config $config, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - ProductRepositoryInterface $productRepository + ProductRepositoryInterface $productRepository, + MsrpPriceCalculatorInterface $msrpPriceCalculator = null ) { parent::__construct($context); $this->storeManager = $storeManager; @@ -67,17 +78,19 @@ public function __construct( $this->config = $config; $this->priceCurrency = $priceCurrency; $this->productRepository = $productRepository; + $this->msrpPriceCalculator = $msrpPriceCalculator + ?: ObjectManager::getInstance()->get(MsrpPriceCalculatorInterface::class); } /** - * Check if can apply Minimum Advertise price to product - * in specific visibility + * Check if can apply Minimum Advertise price to product in specific visibility * * @param int|Product $product * @param int|null $visibility Check displaying price in concrete place (by default generally) * @return bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function canApplyMsrp($product, $visibility = null) { @@ -111,6 +124,7 @@ public function canApplyMsrp($product, $visibility = null) * * @param Product $product * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getMsrpPriceMessage($product) { @@ -128,6 +142,7 @@ public function getMsrpPriceMessage($product) * * @param int|Product $product * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isShowPriceOnGesture($product) { @@ -135,8 +150,11 @@ public function isShowPriceOnGesture($product) } /** + * Check if we should show MAP proce before order confirmation + * * @param int|Product $product * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isShowBeforeOrderConfirm($product) { @@ -144,23 +162,19 @@ public function isShowBeforeOrderConfirm($product) } /** + * Check if any MAP price is larger than as low as value. + * * @param int|Product $product - * @return bool|float + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isMinimalPriceLessMsrp($product) { if (is_numeric($product)) { $product = $this->productRepository->getById($product, false, $this->storeManager->getStore()->getId()); } - $msrp = $product->getMsrp(); + $msrp = $this->msrpPriceCalculator->getMsrpPriceValue($product); $price = $product->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE); - if ($msrp === null) { - if ($product->getTypeId() !== \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { - return false; - } else { - $msrp = $product->getTypeInstance()->getChildrenMsrp($product); - } - } if ($msrp) { $msrp = $this->priceCurrency->convertAndRound($msrp); } diff --git a/app/code/Magento/Msrp/Pricing/MsrpPriceCalculator.php b/app/code/Magento/Msrp/Pricing/MsrpPriceCalculator.php new file mode 100644 index 0000000000000..af5a29eb288ea --- /dev/null +++ b/app/code/Magento/Msrp/Pricing/MsrpPriceCalculator.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Msrp\Pricing; + +use Magento\Catalog\Api\Data\ProductInterface; + +/** + * @inheritdoc + */ +class MsrpPriceCalculator implements MsrpPriceCalculatorInterface +{ + /** + * @var MsrpPriceCalculatorInterface[] + */ + private $msrpPriceCalculators; + + /** + * @param array $msrpPriceCalculators + */ + public function __construct(array $msrpPriceCalculators = []) + { + $this->msrpPriceCalculators = $this->getMsrpPriceCalculators($msrpPriceCalculators); + } + + /** + * @inheritdoc + */ + public function getMsrpPriceValue(ProductInterface $product): float + { + $productType = $product->getTypeId(); + if (isset($this->msrpPriceCalculators[$productType])) { + $priceCalculator = $this->msrpPriceCalculators[$productType]; + $msrp = $priceCalculator->getMsrpPriceValue($product); + } else { + $msrp = (float)$product->getMsrp(); + } + + return $msrp; + } + + /** + * Convert the configuration of MSRP price calculators. + * + * @param array $msrpPriceCalculatorsConfig + * @return array + */ + private function getMsrpPriceCalculators(array $msrpPriceCalculatorsConfig): array + { + $msrpPriceCalculators = []; + foreach ($msrpPriceCalculatorsConfig as $msrpPriceCalculator) { + if (isset($msrpPriceCalculator['productType'], $msrpPriceCalculator['priceCalculator'])) { + $msrpPriceCalculators[$msrpPriceCalculator['productType']] = + $msrpPriceCalculator['priceCalculator']; + } + } + return $msrpPriceCalculators; + } +} diff --git a/app/code/Magento/Msrp/Pricing/MsrpPriceCalculatorInterface.php b/app/code/Magento/Msrp/Pricing/MsrpPriceCalculatorInterface.php new file mode 100644 index 0000000000000..c50a381a2efa4 --- /dev/null +++ b/app/code/Magento/Msrp/Pricing/MsrpPriceCalculatorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Msrp\Pricing; + +use Magento\Catalog\Api\Data\ProductInterface; + +/** + * Provide information about MSRP price of a product. + */ +interface MsrpPriceCalculatorInterface +{ + /** + * Return the value of MSRP product price. + * + * @param ProductInterface $product + * @return float + */ + public function getMsrpPriceValue(ProductInterface $product): float; +} diff --git a/app/code/Magento/Msrp/Pricing/Render/PriceBox.php b/app/code/Magento/Msrp/Pricing/Render/PriceBox.php new file mode 100644 index 0000000000000..892c0bcb51244 --- /dev/null +++ b/app/code/Magento/Msrp/Pricing/Render/PriceBox.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Msrp\Pricing\Render; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\Math\Random; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\Render\RendererPool; +use Magento\Framework\View\Element\Template\Context; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; + +/** + * MSRP price box render. + */ +class PriceBox extends \Magento\Catalog\Pricing\Render\PriceBox +{ + /** + * @var MsrpPriceCalculatorInterface + */ + private $msrpPriceCalculator; + + /** + * Constructor + * + * @param Context $context + * @param Product $saleableItem + * @param PriceInterface $price + * @param RendererPool $rendererPool + * @param Data $jsonHelper + * @param Random $mathRandom + * @param MsrpPriceCalculatorInterface $msrpPriceCalculator + */ + public function __construct( + Context $context, + Product $saleableItem, + PriceInterface $price, + RendererPool $rendererPool, + Data $jsonHelper, + Random $mathRandom, + MsrpPriceCalculatorInterface $msrpPriceCalculator + ) { + $this->msrpPriceCalculator = $msrpPriceCalculator; + parent::__construct($context, $saleableItem, $price, $rendererPool, $jsonHelper, $mathRandom); + } + + /** + * Return MSRP price calculator. + * + * @return MsrpPriceCalculatorInterface + */ + public function getMsrpPriceCalculator(): MsrpPriceCalculatorInterface + { + return $this->msrpPriceCalculator; + } +} diff --git a/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml new file mode 100644 index 0000000000000..3922bb4868914 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml @@ -0,0 +1,24 @@ +<?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="MsrpEnableMAP" type="msrp_settings_config"> + <requiredEntity type="enabled">EnableMAP</requiredEntity> + </entity> + <entity name="EnableMAP" type="msrp_settings_config"> + <data key="value">1</data> + </entity> + + <entity name="MsrpDisableMAP" type="msrp_settings_config"> + <requiredEntity type="enabled">DisableMAP</requiredEntity> + </entity> + <entity name="DisableMAP" type="msrp_settings_config"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml new file mode 100644 index 0000000000000..be91a548ad909 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml @@ -0,0 +1,21 @@ +<?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="MsrpSettingsConfig" dataType="msrp_settings_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST"> + <object key="groups" dataType="msrp_settings_config"> + <object key="msrp" dataType="msrp_settings_config"> + <object key="fields" dataType="msrp_settings_config"> + <object key="enabled" dataType="enabled"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml new file mode 100644 index 0000000000000..a874de3b223a2 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -0,0 +1,157 @@ +<?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="StorefrontProductWithMapAssignedConfigProductIsCorrectTest"> + <annotations> + <features value="Msrp"/> + <title value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <description value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12292"/> + <useCaseId value="MC-10973"/> + <group value="Msrp"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleProductWithPrice50" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleProductWithPrice60" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiSimpleProductWithPrice70" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + + <!--Enable Minimum advertised Price--> + <createData entity="MsrpEnableMAP" stepKey="enableMAP"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!--Disable Minimum advertised Price--> + <createData entity="MsrpDisableMAP" stepKey="disableMAP"/> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!-- Set Manufacturer's Suggested Retail Price to products--> + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct1.id$$)}}" stepKey="goToFirstChildProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="55" stepKey="setMsrpForFirstChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct2.id$$)}}" stepKey="goToSecondChildProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton1"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="66" stepKey="setMsrpForSecondChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Clear cache--> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Go to store front and check msrp for products--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToConfigProductPage"/> + <waitForPageLoad stepKey="waitForLoadConfigProductPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabMapPrice"/> + <assertEquals expected='$66.00' expectedType="string" actual="($grabMapPrice)" stepKey="assertMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLink"/> + + <!--Check msrp for second child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption2.value$$" stepKey="selectSecondOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabSecondProductMapPrice"/> + <assertEquals expected='$66.00' expectedType="string" actual="($grabSecondProductMapPrice)" stepKey="assertSecondProductMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForSecondProduct"/> + + <!--Check msrp for first child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectFirstOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad1"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabFirstProductMapPrice"/> + <assertEquals expected='$55.00' expectedType="string" actual="($grabFirstProductMapPrice)" stepKey="assertFirstProductMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForFirstProduct"/> + + <!--Check price for third child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption3.value$$" stepKey="selectThirdOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad2"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="grabThirdProductMapPrice"/> + <assertEquals expected='$70.00' expectedType="string" actual="($grabThirdProductMapPrice)" stepKey="assertThirdProductMapPrice"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForThirdProduct"/> + </test> +</tests> diff --git a/app/code/Magento/Msrp/Test/Unit/Helper/DataTest.php b/app/code/Magento/Msrp/Test/Unit/Helper/DataTest.php index 19f46b06ac5af..e4cd411a2e0b8 100644 --- a/app/code/Magento/Msrp/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Msrp/Test/Unit/Helper/DataTest.php @@ -6,6 +6,8 @@ namespace Magento\Msrp\Test\Unit\Helper; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; + /** * Class DataTest */ @@ -26,6 +28,14 @@ class DataTest extends \PHPUnit\Framework\TestCase */ protected $productMock; + /** + * @var MsrpPriceCalculatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $msrpPriceCalculator; + + /** + * @inheritdoc + */ protected function setUp() { $this->priceCurrencyMock = $this->createMock(\Magento\Framework\Pricing\PriceCurrencyInterface::class); @@ -33,6 +43,8 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getMsrp', 'getPriceInfo', '__wakeup']) ->getMock(); + $this->msrpPriceCalculator = $this->getMockBuilder(MsrpPriceCalculatorInterface::class) + ->getMockForAbstractClass(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -40,10 +52,14 @@ protected function setUp() \Magento\Msrp\Helper\Data::class, [ 'priceCurrency' => $this->priceCurrencyMock, + 'msrpPriceCalculator' => $this->msrpPriceCalculator, ] ); } + /** + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ public function testIsMinimalPriceLessMsrp() { $msrp = 120; @@ -73,12 +89,13 @@ function ($arg) { ->with(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE) ->will($this->returnValue($finalPriceMock)); - $this->productMock->expects($this->any()) - ->method('getMsrp') - ->will($this->returnValue($msrp)); + $this->msrpPriceCalculator + ->expects($this->any()) + ->method('getMsrpPriceValue') + ->willReturn($msrp); $this->productMock->expects($this->any()) ->method('getPriceInfo') - ->will($this->returnValue($priceInfoMock)); + ->willReturn($priceInfoMock); $result = $this->helper->isMinimalPriceLessMsrp($this->productMock); $this->assertTrue($result, "isMinimalPriceLessMsrp returned incorrect value"); diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json index 6e7bf61063a2a..a2e6da6de5387 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -10,7 +10,6 @@ "magento/module-catalog": "*", "magento/module-downloadable": "*", "magento/module-eav": "*", - "magento/module-grouped-product": "*", "magento/module-store": "*", "magento/module-tax": "*" }, diff --git a/app/code/Magento/Msrp/etc/adminhtml/system.xml b/app/code/Magento/Msrp/etc/adminhtml/system.xml index 8ce0ea67343f8..c20d753a2e794 100644 --- a/app/code/Magento/Msrp/etc/adminhtml/system.xml +++ b/app/code/Magento/Msrp/etc/adminhtml/system.xml @@ -10,7 +10,7 @@ <section id="sales"> <group id="msrp" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Minimum Advertised Price</label> - <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <field id="enabled" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Enable MAP</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> diff --git a/app/code/Magento/Msrp/etc/di.xml b/app/code/Magento/Msrp/etc/di.xml index 6f197f769d412..b8392b0bb0fe4 100644 --- a/app/code/Magento/Msrp/etc/di.xml +++ b/app/code/Magento/Msrp/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="\Magento\Msrp\Api\Data\ProductRender\MsrpPriceInfoInterface" type="\Magento\Msrp\Model\ProductRender\MsrpPriceInfo" /> + <preference for="\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface" type="\Magento\Msrp\Pricing\MsrpPriceCalculator"/> <virtualType name="Magento\Catalog\Pricing\Price\Pool"> <arguments> <argument name="prices" xsi:type="array"> diff --git a/app/code/Magento/Msrp/i18n/de_DE.csv b/app/code/Magento/Msrp/i18n/de_DE.csv deleted file mode 100644 index be8e733a4bfd1..0000000000000 --- a/app/code/Magento/Msrp/i18n/de_DE.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/en_US.csv b/app/code/Magento/Msrp/i18n/en_US.csv index d647f8527ec15..d47d72b2bdc9a 100644 --- a/app/code/Magento/Msrp/i18n/en_US.csv +++ b/app/code/Magento/Msrp/i18n/en_US.csv @@ -13,6 +13,7 @@ Price,Price "Add to Cart","Add to Cart" "Minimum Advertised Price","Minimum Advertised Price" "Enable MAP","Enable MAP" +"<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.","<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront." "Display Actual Price","Display Actual Price" "Default Popup Text Message","Default Popup Text Message" "Default ""What's This"" Text Message","Default ""What's This"" Text Message" diff --git a/app/code/Magento/Msrp/i18n/es_ES.csv b/app/code/Magento/Msrp/i18n/es_ES.csv deleted file mode 100644 index be8e733a4bfd1..0000000000000 --- a/app/code/Magento/Msrp/i18n/es_ES.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/fr_FR.csv b/app/code/Magento/Msrp/i18n/fr_FR.csv deleted file mode 100644 index be8e733a4bfd1..0000000000000 --- a/app/code/Magento/Msrp/i18n/fr_FR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/nl_NL.csv b/app/code/Magento/Msrp/i18n/nl_NL.csv deleted file mode 100644 index be8e733a4bfd1..0000000000000 --- a/app/code/Magento/Msrp/i18n/nl_NL.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/pt_BR.csv b/app/code/Magento/Msrp/i18n/pt_BR.csv deleted file mode 100644 index be8e733a4bfd1..0000000000000 --- a/app/code/Magento/Msrp/i18n/pt_BR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/zh_Hans_CN.csv b/app/code/Magento/Msrp/i18n/zh_Hans_CN.csv deleted file mode 100644 index be8e733a4bfd1..0000000000000 --- a/app/code/Magento/Msrp/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/view/base/layout/catalog_product_prices.xml b/app/code/Magento/Msrp/view/base/layout/catalog_product_prices.xml index 9e2dd20638646..b8a3910bf21bc 100644 --- a/app/code/Magento/Msrp/view/base/layout/catalog_product_prices.xml +++ b/app/code/Magento/Msrp/view/base/layout/catalog_product_prices.xml @@ -11,7 +11,7 @@ <argument name="default" xsi:type="array"> <item name="prices" xsi:type="array"> <item name="msrp_price" xsi:type="array"> - <item name="render_class" xsi:type="string">Magento\Catalog\Pricing\Render\PriceBox</item> + <item name="render_class" xsi:type="string">Magento\Msrp\Pricing\Render\PriceBox</item> <item name="render_template" xsi:type="string">Magento_Msrp::product/price/msrp.phtml</item> </item> </item> diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml index dd5abd433073d..a428df57ab113 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -9,7 +9,7 @@ /** * Template for displaying product price at product view page, gift registry and wish-list * - * @var $block \Magento\Catalog\Pricing\Render\PriceBox + * @var $block \Magento\Msrp\Pricing\Render\PriceBox */ ?> <?php @@ -20,8 +20,11 @@ $priceType = $block->getPrice(); /** @var $product \Magento\Catalog\Model\Product */ $product = $block->getSaleableItem(); $productId = $product->getId(); + +$amount = $block->getMsrpPriceCalculator()->getMsrpPriceValue($product); + $msrpPrice = $block->renderAmount( - $priceType->getCustomAmount($product->getMsrp() ?: $product->getTypeInstance()->getChildrenMsrp($product)), + $priceType->getCustomAmount($amount), [ 'price_id' => $block->getPriceId() ? $block->getPriceId() : 'old-price-' . $productId, 'include_container' => false, @@ -29,54 +32,56 @@ $msrpPrice = $block->renderAmount( ] ); $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-'; - -$addToCartUrl = ''; -if ($product->isSaleable()) { - /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ - $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); - $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( - $product, - ['_query' => [ - \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => - $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( - $addToCartUrlGenerator->getAddToCartUrl($product) - ), - ]] - ); -} ?> -<?php if ($product->getMsrp()): ?> + +<?php if ($amount): ?> <span class="old-price map-old-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> + <span class="map-fallback-price normal-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> <?php endif; ?> <?php if ($priceType->isShowPriceOnGesture()): ?> <?php - $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); - $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); - $data = ['addToCart' => [ - 'origin'=> 'msrp', - 'popupId' => '#' . $popupId, - 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), - 'productId' => $productId, - 'productIdInput' => 'input[type="hidden"][name="product"]', - 'realPrice' => $block->getRealPriceHtml(), - 'isSaleable' => $product->isSaleable(), - 'msrpPrice' => $msrpPrice, - 'priceElementId' => $priceElementId, - 'closeButtonId' => '#map-popup-close', - 'addToCartUrl' => $addToCartUrl, - 'paymentButtons' => '[data-label=or]' - ]]; - if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { - $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; - } else { - $data['addToCart']['addToCartButton'] = sprintf( - 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', - (int) $productId) . ',' . - sprintf('.block.widget .price-box[data-product-id=%s]+.product-item-actions button.tocart', - (int) $productId - ); - } + + $addToCartUrl = ''; + if ($product->isSaleable()) { + /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ + $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); + $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( + $product, + ['_query' => [ + \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => + $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( + $addToCartUrlGenerator->getAddToCartUrl($product) + ), + ]] + ); + } + + $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); + $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); + $data = ['addToCart' => [ + 'origin'=> 'msrp', + 'popupId' => '#' . $popupId, + 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), + 'productId' => $productId, + 'productIdInput' => 'input[type="hidden"][name="product"]', + 'realPrice' => $block->getRealPriceHtml(), + 'isSaleable' => $product->isSaleable(), + 'msrpPrice' => $msrpPrice, + 'priceElementId' => $priceElementId, + 'closeButtonId' => '#map-popup-close', + 'addToCartUrl' => $addToCartUrl, + 'paymentButtons' => '[data-label=or]' + ]]; + if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { + $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; + } else { + $data['addToCart']['addToCartButton'] = sprintf( + 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', + (int) $productId . ',' . + sprintf('.block.widget .price-box[data-product-id=%s]+.product-item-actions button.tocart', + (int) $productId)); + } ?> <span id="<?= /* @escapeNotVerified */ $block->getPriceId() ? $block->getPriceId() : $priceElementId ?>" style="display:none"></span> <a href="javascript:void(0);" @@ -100,4 +105,4 @@ if ($product->isSaleable()) { "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", "closeButtonId": "#map-popup-close"}}'><span><?= /* @escapeNotVerified */ __("What's this?") ?></span> </a> -<?php endif; ?> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Msrp/view/base/web/js/msrp.js b/app/code/Magento/Msrp/view/base/web/js/msrp.js index deeadd9b55b82..a0bd3ec132de6 100644 --- a/app/code/Magento/Msrp/view/base/web/js/msrp.js +++ b/app/code/Magento/Msrp/view/base/web/js/msrp.js @@ -4,11 +4,12 @@ */ define([ 'jquery', + 'Magento_Catalog/js/price-utils', 'underscore', 'jquery/ui', 'mage/dropdown', 'mage/template' -], function ($) { +], function ($, priceUtils, _) { 'use strict'; $.widget('mage.addToCart', { @@ -24,7 +25,14 @@ define([ // Selectors cartForm: '.form.map.checkout', msrpLabelId: '#map-popup-msrp', + msrpPriceElement: '#map-popup-msrp .price-wrapper', priceLabelId: '#map-popup-price', + priceElement: '#map-popup-price .price', + mapInfoLinks: '.map-show-info', + displayPriceElement: '.old-price.map-old-price .price-wrapper', + fallbackPriceElement: '.normal-price.map-fallback-price .price-wrapper', + displayPriceContainer: '.old-price.map-old-price', + fallbackPriceContainer: '.normal-price.map-fallback-price', popUpAttr: '[data-role=msrp-popup-template]', popupCartButtonId: '#map-popup-button', paypalCheckoutButons: '[data-action=checkout-form-submit]', @@ -59,9 +67,11 @@ define([ shadowHinter: 'popup popup-pointer' }, popupOpened: false, + wasOpened: false, /** * Creates widget instance + * * @private */ _create: function () { @@ -73,10 +83,13 @@ define([ this.initTierPopup(); } $(this.options.cartButtonId).on('click', this._addToCartSubmit.bind(this)); + $(document).on('updateMsrpPriceBlock', this.onUpdateMsrpPrice.bind(this)); + $(this.options.cartForm).on('submit', this._onSubmitForm.bind(this)); }, /** * Init msrp popup + * * @private */ initMsrpPopup: function () { @@ -89,7 +102,7 @@ define([ $msrpPopup.find('button') .on('click', - this.handleMsrpAddToCart.bind(this)) + this.handleMsrpAddToCart.bind(this)) .filter(this.options.popupCartButtonId) .text($(this.options.addToCartButton).text()); @@ -104,6 +117,7 @@ define([ /** * Init info popup + * * @private */ initInfoPopup: function () { @@ -212,8 +226,12 @@ define([ var options = this.tierOptions || this.options; this.popUpOptions.position.of = $(event.target); - this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); - this.$popup.find(this.options.priceLabelId).html(options.realPrice); + + if (!this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + this.wasOpened = true; + } this.$popup.dropdownDialog(this.popUpOptions).dropdownDialog('open'); this._toggle(this.$popup); @@ -223,6 +241,7 @@ define([ }, /** + * Toggle MAP popup visibility * * @param {HTMLElement} $elem * @private @@ -239,6 +258,7 @@ define([ }, /** + * Close MAP information popup * * @param {HTMLElement} $elem */ @@ -249,8 +269,10 @@ define([ /** * Handler for addToCart action + * + * @param {Object} e */ - _addToCartSubmit: function () { + _addToCartSubmit: function (e) { this.element.trigger('addToCart', this.element); if (this.element.data('stop-processing')) { @@ -266,9 +288,106 @@ define([ if (this.options.addToCartUrl) { $('.mage-dropdown-dialog > .ui-dialog-content').dropdownDialog('close'); } + + e.preventDefault(); $(this.options.cartForm).submit(); + }, + /** + * Call on event updatePrice. Proxy to updateMsrpPrice method. + * + * @param {Event} event + * @param {mixed} priceIndex + * @param {Object} prices + */ + onUpdateMsrpPrice: function onUpdateMsrpPrice(event, priceIndex, prices) { + + var defaultMsrp, + defaultPrice, + msrpPrice, + finalPrice; + + defaultMsrp = _.chain(prices).map(function (price) { + return price.msrpPrice.amount; + }).reject(function (p) { + return p === null; + }).max().value(); + + defaultPrice = _.chain(prices).map(function (p) { + return p.finalPrice.amount; + }).min().value(); + + if (typeof priceIndex !== 'undefined') { + msrpPrice = prices[priceIndex].msrpPrice.amount; + finalPrice = prices[priceIndex].finalPrice.amount; + + if (msrpPrice === null || msrpPrice <= finalPrice) { + this.updateNonMsrpPrice(priceUtils.formatPrice(finalPrice)); + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(finalPrice), + priceUtils.formatPrice(msrpPrice), + false); + } + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(defaultPrice), + priceUtils.formatPrice(defaultMsrp), + true); + } + }, + + /** + * Update prices for configurable product with MSRP enabled + * + * @param {String} finalPrice + * @param {String} msrpPrice + * @param {Boolean} useDefaultPrice + */ + updateMsrpPrice: function (finalPrice, msrpPrice, useDefaultPrice) { + var options = this.tierOptions || this.options; + + $(this.options.fallbackPriceContainer).hide(); + $(this.options.displayPriceContainer).show(); + $(this.options.mapInfoLinks).show(); + + if (useDefaultPrice || !this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + $(this.options.displayPriceElement).html(msrpPrice); + this.wasOpened = true; + } + + if (!useDefaultPrice) { + this.$popup.find(this.options.msrpPriceElement).html(msrpPrice); + this.$popup.find(this.options.priceElement).html(finalPrice); + $(this.options.displayPriceElement).html(msrpPrice); + } + }, + + /** + * Display non MAP price for irrelevant products + * + * @param {String} price + */ + updateNonMsrpPrice: function (price) { + $(this.options.fallbackPriceElement).html(price); + $(this.options.displayPriceContainer).hide(); + $(this.options.mapInfoLinks).hide(); + $(this.options.fallbackPriceContainer).show(); + }, + + /** + * Handler for submit form + * + * @private + */ + _onSubmitForm: function () { + if ($(this.options.cartForm).valid()) { + $(this.options.cartButtonId).prop('disabled', true); + } } + }); return $.mage.addToCart; diff --git a/app/code/Magento/MsrpConfigurableProduct/Pricing/MsrpPriceCalculator.php b/app/code/Magento/MsrpConfigurableProduct/Pricing/MsrpPriceCalculator.php new file mode 100644 index 0000000000000..b6f5194ab8cbe --- /dev/null +++ b/app/code/Magento/MsrpConfigurableProduct/Pricing/MsrpPriceCalculator.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MsrpConfigurableProduct\Pricing; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; + +/** + * {@inheritdoc}. Provide information for a Configurable product. + */ +class MsrpPriceCalculator implements MsrpPriceCalculatorInterface +{ + /** + * @inheritdoc + */ + public function getMsrpPriceValue(ProductInterface $product): float + { + /** @var Product $product */ + if ($product->getTypeId() !== Configurable::TYPE_CODE) { + return 0; + } + + /** @var Configurable $configurableProduct */ + $configurableProduct = $product->getTypeInstance(); + $msrp = 0; + $prices = []; + foreach ($configurableProduct->getUsedProducts($product) as $item) { + if ($item->getMsrp() !== null) { + $prices[] = $item->getMsrp(); + } + } + if ($prices) { + $msrp = (float)max($prices); + } + + return $msrp; + } +} diff --git a/app/code/Magento/MsrpConfigurableProduct/README.md b/app/code/Magento/MsrpConfigurableProduct/README.md new file mode 100644 index 0000000000000..8911b6e9e6667 --- /dev/null +++ b/app/code/Magento/MsrpConfigurableProduct/README.md @@ -0,0 +1,3 @@ +# MsrpConfigurableProduct + +**MsrpConfigurableProduct** provides type and resolver information for the Msrp module from the ConfigurableProduct module. \ No newline at end of file diff --git a/app/code/Magento/MsrpConfigurableProduct/composer.json b/app/code/Magento/MsrpConfigurableProduct/composer.json new file mode 100644 index 0000000000000..00c3cf6b03078 --- /dev/null +++ b/app/code/Magento/MsrpConfigurableProduct/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-msrp-configurable-product", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-msrp": "*", + "magento/module-configurable-product": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MsrpConfigurableProduct\\": "" + } + } +} diff --git a/app/code/Magento/MsrpConfigurableProduct/etc/di.xml b/app/code/Magento/MsrpConfigurableProduct/etc/di.xml new file mode 100644 index 0000000000000..ea33c81ff7cf5 --- /dev/null +++ b/app/code/Magento/MsrpConfigurableProduct/etc/di.xml @@ -0,0 +1,19 @@ +<?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\Msrp\Pricing\MsrpPriceCalculator"> + <arguments> + <argument name="msrpPriceCalculators" xsi:type="array"> + <item name="configurable" xsi:type="array"> + <item name="productType" xsi:type="const">\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE</item> + <item name="priceCalculator" xsi:type="object">\Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator</item> + </item> + </argument> + </arguments> + </type> +</config> \ No newline at end of file diff --git a/app/code/Magento/MsrpConfigurableProduct/etc/module.xml b/app/code/Magento/MsrpConfigurableProduct/etc/module.xml new file mode 100644 index 0000000000000..b00e363b0b269 --- /dev/null +++ b/app/code/Magento/MsrpConfigurableProduct/etc/module.xml @@ -0,0 +1,16 @@ +<?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_MsrpConfigurableProduct" > + <sequence> + <module name="Magento_Catalog"/> + <module name="Magento_Msrp"/> + <module name="Magento_ConfigurableProduct"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MsrpConfigurableProduct/registration.php b/app/code/Magento/MsrpConfigurableProduct/registration.php new file mode 100644 index 0000000000000..d4d58ec3c013b --- /dev/null +++ b/app/code/Magento/MsrpConfigurableProduct/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MsrpConfigurableProduct', __DIR__); diff --git a/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php b/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php new file mode 100644 index 0000000000000..b99f328a8b200 --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MsrpGroupedProduct\Pricing; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; + +/** + * {@inheritdoc}. Provide information for a Grouped product. + */ +class MsrpPriceCalculator implements MsrpPriceCalculatorInterface +{ + /** + * @inheritdoc + */ + public function getMsrpPriceValue(ProductInterface $product): float + { + /** @var Product $product */ + if ($product->getTypeId() !== Grouped::TYPE_CODE) { + return 0; + } + + /** @var Grouped $groupedProduct */ + $groupedProduct = $product->getTypeInstance(); + + return $groupedProduct->getChildrenMsrp($product); + } +} diff --git a/app/code/Magento/MsrpGroupedProduct/README.md b/app/code/Magento/MsrpGroupedProduct/README.md new file mode 100644 index 0000000000000..d597ba7fc18a7 --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/README.md @@ -0,0 +1,3 @@ +# MsrpGroupedProduct + +**MsrpGroupedProduct** provides type and resolver information for the Msrp module from the GroupedProduct module. \ No newline at end of file diff --git a/app/code/Magento/MsrpGroupedProduct/composer.json b/app/code/Magento/MsrpGroupedProduct/composer.json new file mode 100644 index 0000000000000..a626f199ad6cc --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-msrp-grouped-product", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-msrp": "*", + "magento/module-grouped-product": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MsrpGroupedProduct\\": "" + } + } +} diff --git a/app/code/Magento/MsrpGroupedProduct/etc/di.xml b/app/code/Magento/MsrpGroupedProduct/etc/di.xml new file mode 100644 index 0000000000000..29b25f15bc2c1 --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/etc/di.xml @@ -0,0 +1,19 @@ +<?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\Msrp\Pricing\MsrpPriceCalculator"> + <arguments> + <argument name="msrpPriceCalculators" xsi:type="array"> + <item name="grouped" xsi:type="array"> + <item name="productType" xsi:type="const">\Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE</item> + <item name="priceCalculator" xsi:type="object">\Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator</item> + </item> + </argument> + </arguments> + </type> +</config> \ No newline at end of file diff --git a/app/code/Magento/MsrpGroupedProduct/etc/module.xml b/app/code/Magento/MsrpGroupedProduct/etc/module.xml new file mode 100644 index 0000000000000..898dd904b5dfb --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/etc/module.xml @@ -0,0 +1,16 @@ +<?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_MsrpGroupedProduct" > + <sequence> + <module name="Magento_Catalog"/> + <module name="Magento_Msrp"/> + <module name="Magento_GroupedProduct"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MsrpGroupedProduct/registration.php b/app/code/Magento/MsrpGroupedProduct/registration.php new file mode 100644 index 0000000000000..c5a261e66c640 --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MsrpGroupedProduct', __DIR__); diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index d1df064f57140..42f5289d2109a 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -1182,7 +1182,7 @@ private function removePlacedItemsFromQuote(array $shippingAddresses, array $pla { foreach ($shippingAddresses as $address) { foreach ($address->getAllItems() as $addressItem) { - if (in_array($addressItem->getId(), $placedAddressItems)) { + if (in_array($addressItem->getQuoteItemId(), $placedAddressItems)) { if ($addressItem->getProduct()->getIsVirtual()) { $addressItem->isDeleted(true); } else { @@ -1232,7 +1232,7 @@ private function searchQuoteAddressId(OrderInterface $order, array $addresses): $item = array_pop($items); foreach ($addresses as $address) { foreach ($address->getAllItems() as $addressItem) { - if ($addressItem->getId() == $item->getQuoteItemId()) { + if ($addressItem->getQuoteItemId() == $item->getQuoteItemId()) { return (int)$address->getId(); } } diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml index c6bcdeb7b0413..fee3cb790a522 100644 --- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml +++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml @@ -12,6 +12,7 @@ <block class="Magento\Customer\Block\Address\Edit" name="customer_address_edit" template="Magento_Customer::address/edit.phtml" cacheable="false"> <arguments> <argument name="attribute_data" xsi:type="object">Magento\Customer\Block\DataProviders\AddressAttributeData</argument> + <argument name="post_code_config" xsi:type="object">Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml index da553b7823da9..4354cfb7c1c3e 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml @@ -55,6 +55,10 @@ <div class="box-content"> <address> <?= /* @noEscape */ $block->getCheckoutData()->getAddressHtml($block->getAddress()); ?> + <input type="hidden" + id="multishipping_billing_country_id" + value="<?= /* @noEscape */ $block->getAddress()->getCountryId(); ?>" + name="multishipping_billing_country_id"/> </address> </div> </div> @@ -80,35 +84,44 @@ $block->setMethodFormTemplate($code, $methodsForms[$code]); } ?> - <dt class="item-title"> - <?php if ($methodsCount > 1) : ?> - <input type="radio" - id="p_method_<?= $block->escapeHtml($code); ?>" - value="<?= $block->escapeHtml($code); ?>" - name="payment[method]" - title="<?= $block->escapeHtml($_method->getTitle()) ?>" - <?php if ($checked) : ?> - checked="checked" + <div data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> + <dt class="item-title"> + <?php if ($methodsCount > 1) : ?> + <input type="radio" + id="p_method_<?= $block->escapeHtml($code); ?>" + value="<?= $block->escapeHtml($code); ?>" + name="payment[method]" + title="<?= $block->escapeHtml($_method->getTitle()) ?>" + data-bind=" + value: getCode(), + checked: isChecked, + click: selectPaymentMethod, + visible: isRadioButtonVisible()" + <?php if ($checked) : ?> + checked="checked" + <?php endif; ?> + class="radio"/> + <?php else : ?> + <input type="radio" + id="p_method_<?= $block->escapeHtml($code); ?>" + value="<?= $block->escapeHtml($code); ?>" + name="payment[method]" + data-bind=" + value: getCode(), + afterRender: selectPaymentMethod" + checked="checked" + class="radio solo method" /> <?php endif; ?> - class="radio"/> - <?php else : ?> - <input type="radio" - id="p_method_<?= $block->escapeHtml($code); ?>" - value="<?= $block->escapeHtml($code); ?>" - name="payment[method]" - checked="checked" - class="radio solo method" /> - <?php endif; ?> - <label for="p_method_<?= $block->escapeHtml($code); ?>"> - <?= $block->escapeHtml($_method->getTitle()) ?> - </label> - </dt> - <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> - <dd class="item-content <?= $checked ? '' : 'no-display'; ?>" - data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> - <?= /* @noEscape */ $html; ?> - </dd> - <?php endif; ?> + <label for="p_method_<?= $block->escapeHtml($code); ?>"> + <?= $block->escapeHtml($_method->getTitle()) ?> + </label> + </dt> + <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> + <dd class="item-content <?= $checked ? '' : 'no-display'; ?>"> + <?= /* @noEscape */ $html; ?> + </dd> + <?php endif; ?> + </div> <?php endforeach; ?> </dl> <?= $block->getChildHtml('payment_methods_after') ?> diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js index 9b867cd7217b1..3a6d73e304974 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js @@ -15,7 +15,7 @@ define([ opacity: 0.5, // CSS opacity for the 'Place Order' button when it's clicked and then disabled. pleaseWaitLoader: 'span.please-wait', // 'Submitting order information...' Ajax loader. placeOrderSubmit: 'button[type="submit"]', // The 'Place Order' button. - agreements: '#checkout-agreements' // Container for all of the checkout agreements and terms/conditions + agreements: '.checkout-agreements' // Container for all of the checkout agreements and terms/conditions }, /** diff --git a/app/code/Magento/MysqlMq/Model/Driver/Queue.php b/app/code/Magento/MysqlMq/Model/Driver/Queue.php index b8dab6fac7b24..cbc2e951782f2 100644 --- a/app/code/Magento/MysqlMq/Model/Driver/Queue.php +++ b/app/code/Magento/MysqlMq/Model/Driver/Queue.php @@ -73,7 +73,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function dequeue() { @@ -92,7 +92,7 @@ public function dequeue() } /** - * {@inheritdoc} + * @inheritdoc */ public function acknowledge(EnvelopeInterface $envelope) { @@ -103,25 +103,26 @@ public function acknowledge(EnvelopeInterface $envelope) } /** - * {@inheritdoc} + * @inheritdoc */ public function subscribe($callback) { while (true) { while ($envelope = $this->dequeue()) { try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func($callback, $envelope); - $this->acknowledge($envelope); } catch (\Exception $e) { $this->reject($envelope); } } + // phpcs:ignore Magento2.Functions.DiscouragedFunction sleep($this->interval); } } /** - * {@inheritdoc} + * @inheritdoc */ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionMessage = null) { @@ -139,7 +140,7 @@ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionM } /** - * {@inheritDoc} + * @inheritDoc */ public function push(EnvelopeInterface $envelope) { diff --git a/app/code/Magento/NewRelicReporting/Model/Module/Collect.php b/app/code/Magento/NewRelicReporting/Model/Module/Collect.php index 7e381762f5d27..fe5389e258aa5 100644 --- a/app/code/Magento/NewRelicReporting/Model/Module/Collect.php +++ b/app/code/Magento/NewRelicReporting/Model/Module/Collect.php @@ -11,6 +11,9 @@ use Magento\NewRelicReporting\Model\Config; use Magento\NewRelicReporting\Model\Module; +/** + * Class for collecting data for the report + */ class Collect { /** @@ -92,7 +95,6 @@ protected function getAllModules() * @param string $active * @param string $setupVersion * @param string $state - * * @return array */ protected function getNewModuleChanges($moduleName, $active, $setupVersion, $state) @@ -277,9 +279,7 @@ public function getModuleData($refresh = true) $changes = array_diff($module, $changeTest); $changesCleanArray = $this->getCleanChangesArray($changes); - if (count($changesCleanArray) > 0 || - ($this->moduleManager->isOutputEnabled($changeTest['name']) && - $module['setup_version'] != null)) { + if (!empty($changesCleanArray)) { $data = [ 'entity_id' => $changeTest['entity_id'], 'name' => $changeTest['name'], diff --git a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php index 9882a1ce9b0b8..bce42b4e90074 100644 --- a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php +++ b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php @@ -21,7 +21,7 @@ class NewRelicWrapper */ public function addCustomParameter($param, $value) { - if (extension_loaded('newrelic')) { + if ($this->isExtensionInstalled()) { newrelic_add_custom_parameter($param, $value); return true; } @@ -36,7 +36,7 @@ public function addCustomParameter($param, $value) */ public function reportError($exception) { - if (extension_loaded('newrelic')) { + if ($this->isExtensionInstalled()) { newrelic_notice_error($exception->getMessage(), $exception); } } @@ -49,11 +49,24 @@ public function reportError($exception) */ public function setAppName(string $appName) { - if (extension_loaded('newrelic')) { + if ($this->isExtensionInstalled()) { newrelic_set_appname($appName); } } + /** + * Wrapper for 'newrelic_name_transaction' + * + * @param string $transactionName + * @return void + */ + public function setTransactionName(string $transactionName): void + { + if ($this->isExtensionInstalled()) { + newrelic_name_transaction($transactionName); + } + } + /** * Checks whether newrelic-php5 agent is installed * diff --git a/app/code/Magento/NewRelicReporting/Plugin/CommandPlugin.php b/app/code/Magento/NewRelicReporting/Plugin/CommandPlugin.php new file mode 100644 index 0000000000000..04ad3d0504d34 --- /dev/null +++ b/app/code/Magento/NewRelicReporting/Plugin/CommandPlugin.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\NewRelicReporting\Plugin; + +use Magento\NewRelicReporting\Model\Config; +use Magento\NewRelicReporting\Model\NewRelicWrapper; +use Symfony\Component\Console\Command\Command; + +/** + * Describe NewRelic commands plugin. + */ +class CommandPlugin +{ + /** + * @var Config + */ + private $config; + + /** + * @var NewRelicWrapper + */ + private $newRelicWrapper; + + /** + * @param Config $config + * @param NewRelicWrapper $newRelicWrapper + */ + public function __construct( + Config $config, + NewRelicWrapper $newRelicWrapper + ) { + $this->config = $config; + $this->newRelicWrapper = $newRelicWrapper; + } + + /** + * Set NewRelic Transaction name before running command. + * + * @param Command $command + * @param array $args + * @return array + */ + public function beforeRun(Command $command, ...$args) + { + $this->newRelicWrapper->setTransactionName( + sprintf('CLI %s', $command->getName()) + ); + + return $args; + } +} diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php index 8d8e6255ab8d3..4286406d6e9ab 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php @@ -162,10 +162,6 @@ public function testGetModuleDataWithoutRefresh() ->method('getNames') ->willReturn($enabledModulesMockArray); - $this->moduleManagerMock->expects($this->any())->method('isOutputEnabled')->will( - $this->returnValue(false) - ); - $this->assertInternalType( 'array', $this->model->getModuleData() @@ -256,10 +252,6 @@ public function testGetModuleDataRefresh($data) ->method('getNames') ->willReturn($enabledModulesMockArray); - $this->moduleManagerMock->expects($this->any())->method('isOutputEnabled')->will( - $this->returnValue(true) - ); - $this->assertInternalType( 'array', $this->model->getModuleData() @@ -350,10 +342,6 @@ public function testGetModuleDataRefreshOrStatement($data) ->method('getNames') ->willReturn($enabledModulesMockArray); - $this->moduleManagerMock->expects($this->any())->method('isOutputEnabled')->will( - $this->returnValue(true) - ); - $this->assertInternalType( 'array', $this->model->getModuleData() diff --git a/app/code/Magento/NewRelicReporting/etc/db_schema.xml b/app/code/Magento/NewRelicReporting/etc/db_schema.xml index b5db533f90c75..c6e61b88f4b1b 100644 --- a/app/code/Magento/NewRelicReporting/etc/db_schema.xml +++ b/app/code/Magento/NewRelicReporting/etc/db_schema.xml @@ -38,8 +38,8 @@ comment="Entity ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer ID"/> - <column xsi:type="decimal" name="total" scale="0" precision="10" unsigned="true" nullable="true"/> - <column xsi:type="decimal" name="total_base" scale="0" precision="10" unsigned="true" nullable="true"/> + <column xsi:type="decimal" name="total" scale="4" precision="20" unsigned="true" nullable="true"/> + <column xsi:type="decimal" name="total_base" scale="4" precision="20" unsigned="true" nullable="true"/> <column xsi:type="int" name="item_count" padding="10" unsigned="true" nullable="false" identity="false" comment="Line Item Count"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml index bab7d6611f14b..15516f6df89be 100644 --- a/app/code/Magento/NewRelicReporting/etc/di.xml +++ b/app/code/Magento/NewRelicReporting/etc/di.xml @@ -40,4 +40,7 @@ </argument> </arguments> </type> + <type name="Symfony\Component\Console\Command\Command"> + <plugin name="newrelic-describe-commands" type="Magento\NewRelicReporting\Plugin\CommandPlugin"/> + </type> </config> diff --git a/app/code/Magento/Newsletter/Controller/Manage/Save.php b/app/code/Magento/Newsletter/Controller/Manage/Save.php index 419cbac10ffd1..698c2d19aae68 100644 --- a/app/code/Magento/Newsletter/Controller/Manage/Save.php +++ b/app/code/Magento/Newsletter/Controller/Manage/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,9 +7,15 @@ namespace Magento\Newsletter\Controller\Manage; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Newsletter\Model\Subscriber; -class Save extends \Magento\Newsletter\Controller\Manage +/** + * Customers newsletter subscription save controller + */ +class Save extends \Magento\Newsletter\Controller\Manage implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Framework\Data\Form\FormKey\Validator @@ -81,6 +86,8 @@ public function execute() $isSubscribedParam = (boolean)$this->getRequest() ->getParam('is_subscribed', false); if ($isSubscribedParam !== $isSubscribedState) { + // No need to validate customer and customer address while saving subscription preferences + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); if ($isSubscribedParam) { $subscribeModel = $this->subscriberFactory->create() @@ -105,4 +112,15 @@ public function execute() } $this->_redirect('customer/account/'); } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php b/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php index 4e338c2d1df34..c27717f4c7793 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php @@ -4,13 +4,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; -class Confirm extends \Magento\Newsletter\Controller\Subscriber +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Confirm subscription controller. + */ +class Confirm extends \Magento\Newsletter\Controller\Subscriber implements HttpGetActionInterface { /** - * Subscription confirm action - * @return void + * Subscription confirm action. + * + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -23,17 +30,17 @@ public function execute() if ($subscriber->getId() && $subscriber->getCode()) { if ($subscriber->confirm($code)) { - $this->messageManager->addSuccess(__('Your subscription has been confirmed.')); + $this->messageManager->addSuccessMessage(__('Your subscription has been confirmed.')); } else { - $this->messageManager->addError(__('This is an invalid subscription confirmation code.')); + $this->messageManager->addErrorMessage(__('This is an invalid subscription confirmation code.')); } } else { - $this->messageManager->addError(__('This is an invalid subscription ID.')); + $this->messageManager->addErrorMessage(__('This is an invalid subscription ID.')); } } - - $resultRedirect = $this->resultRedirectFactory->create(); - $resultRedirect->setUrl($this->_storeManager->getStore()->getBaseUrl()); - return $resultRedirect; + /** @var \Magento\Framework\Controller\Result\Redirect $redirect */ + $redirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); + $redirectUrl = $this->_storeManager->getStore()->getBaseUrl(); + return $redirect->setUrl($redirectUrl); } } diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php index 4f46c84894f12..7557f1610b4f4 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php @@ -4,6 +4,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement; @@ -131,7 +132,7 @@ protected function validateEmailFormat($email) /** * New subscription action * - * @return void + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -160,7 +161,10 @@ public function execute() $this->messageManager->addExceptionMessage($e, __('Something went wrong with the subscription.')); } } - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + /** @var \Magento\Framework\Controller\Result\Redirect $redirect */ + $redirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); + $redirectUrl = $this->_redirect->getRedirectUrl(); + return $redirect->setUrl($redirectUrl); } /** diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php index 88fa128162700..e37a3786e140a 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; @@ -15,7 +16,7 @@ class Unsubscribe extends \Magento\Newsletter\Controller\Subscriber implements H /** * Unsubscribe newsletter. * - * @return \Magento\Backend\Model\View\Result\Redirect + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -25,14 +26,14 @@ public function execute() if ($id && $code) { try { $this->_subscriberFactory->create()->load($id)->setCheckCode($code)->unsubscribe(); - $this->messageManager->addSuccess(__('You unsubscribed.')); + $this->messageManager->addSuccessMessage(__('You unsubscribed.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addException($e, $e->getMessage()); + $this->messageManager->addErrorMessage($e, $e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while unsubscribing you.')); + $this->messageManager->addErrorMessage($e, __('Something went wrong while unsubscribing you.')); } } - /** @var \Magento\Backend\Model\View\Result\Redirect $redirect */ + /** @var \Magento\Framework\Controller\Result\Redirect $redirect */ $redirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); $redirectUrl = $this->_redirect->getRedirectUrl(); return $redirect->setUrl($redirectUrl); diff --git a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php index 035013e572833..309bfadab41b3 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php +++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php @@ -6,13 +6,11 @@ namespace Magento\Newsletter\Model\Plugin; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; -use Magento\Customer\Api\Data\CustomerExtensionInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Newsletter\Model\SubscriberFactory; use Magento\Framework\Api\ExtensionAttributesFactory; -use Magento\Framework\App\ObjectManager; use Magento\Newsletter\Model\ResourceModel\Subscriber; -use Magento\Newsletter\Model\SubscriberFactory; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Api\Data\CustomerExtensionInterface; /** * Newsletter Plugin for customer @@ -41,29 +39,21 @@ class CustomerPlugin */ private $customerSubscriptionStatus = []; - /** - * @var StoreManagerInterface - */ - private $storeManager; - /** * Initialize dependencies. * * @param SubscriberFactory $subscriberFactory * @param ExtensionAttributesFactory $extensionFactory * @param Subscriber $subscriberResource - * @param StoreManagerInterface|null $storeManager */ public function __construct( SubscriberFactory $subscriberFactory, ExtensionAttributesFactory $extensionFactory, - Subscriber $subscriberResource, - StoreManagerInterface $storeManager = null + Subscriber $subscriberResource ) { $this->subscriberFactory = $subscriberFactory; $this->extensionFactory = $extensionFactory; $this->subscriberResource = $subscriberResource; - $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -164,8 +154,7 @@ public function afterDelete(CustomerRepository $subject, $result, CustomerInterf public function afterGetById(CustomerRepository $subject, CustomerInterface $customer) { $extensionAttributes = $customer->getExtensionAttributes(); - $storeId = $this->storeManager->getStore()->getId(); - $customer->setStoreId($storeId); + if ($extensionAttributes === null) { /** @var CustomerExtensionInterface $extensionAttributes */ $extensionAttributes = $this->extensionFactory->create(CustomerInterface::class); diff --git a/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..1df1cd5f8dae8 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?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="AdminMenuMarketingCommunicationsNewsletterQueue"> + <data key="pageTitle">Newsletter Queue</data> + <data key="title">Newsletter Queue</data> + <data key="dataUiId">magento-newsletter-newsletter-queue</data> + </entity> + <entity name="AdminMenuMarketingCommunicationsNewsletterSubscribers"> + <data key="pageTitle">Newsletter Subscribers</data> + <data key="title">Newsletter Subscribers</data> + <data key="dataUiId">magento-newsletter-newsletter-subscriber</data> + </entity> + <entity name="AdminMenuMarketingCommunicationsNewsletterTemplate"> + <data key="pageTitle">Newsletter Template</data> + <data key="title">Newsletter Template</data> + <data key="dataUiId">magento-newsletter-newsletter-template</data> + </entity> + <entity name="AdminMenuReportsMarketingNewsletterProblemReports"> + <data key="pageTitle">Newsletter Problems Report</data> + <data key="title">Newsletter Problem Reports</data> + <data key="dataUiId">magento-newsletter-newsletter-problem</data> + </entity> +</entities> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml index 06f762900436e..bb651784d4dcf 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml @@ -7,8 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerCreateFormSection"> <element name="signUpForNewsletter" type="checkbox" selector="//span[contains(text(), 'Sign Up for Newsletter')]"/> </section> @@ -16,5 +15,4 @@ <section name="CustomerMyAccountPage"> <element name="DescriptionNewsletter" type="text" selector=".box-newsletter p"/> </section> - -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml new file mode 100644 index 0000000000000..31da588250a0a --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingNewsletterQueueNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing newsletter queue navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Newsletter Queue"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14189"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterQueuePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterQueue.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsNewsletterQueue.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml new file mode 100644 index 0000000000000..8ced2690322f8 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingNewsletterSubscribersNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing newsletter subscribers navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Newsletter Subscribers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14190"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterSubscribersPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterSubscribers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsNewsletterSubscribers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml new file mode 100644 index 0000000000000..ca994aa1d6269 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingNewsletterTemplateNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing newsletter template navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Newsletter Template"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14188"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterTemplatePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterTemplate.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsNewsletterTemplate.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml new file mode 100644 index 0000000000000..3891b90536a17 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsNewsletterProblemReportsNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin reports newsletter problem reports navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Newsletter Problem Reports"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14191"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterProblemsReportPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingNewsletterProblemReports.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingNewsletterProblemReports.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index faed8b1af952e..22ca214c94aec 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="VerifySubscribedNewsletterDisplayedTest"> <annotations> <features value="Newsletter"/> @@ -46,6 +47,8 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="Second"/> </actionGroup> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> </after> @@ -63,4 +66,3 @@ </actionGroup> </test> </tests> - diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php index 3be28cacc93e0..e809b7e37a432 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php @@ -10,8 +10,6 @@ use Magento\Customer\Api\Data\CustomerExtensionInterface; use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Newsletter\Model\ResourceModel\Subscriber; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; class CustomerPluginTest extends \PHPUnit\Framework\TestCase { @@ -55,11 +53,6 @@ class CustomerPluginTest extends \PHPUnit\Framework\TestCase */ private $customerMock; - /** - * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $storeManagerMock; - protected function setUp() { $this->subscriberFactory = $this->getMockBuilder(\Magento\Newsletter\Model\SubscriberFactory::class) @@ -94,8 +87,6 @@ protected function setUp() ->setMethods(['getExtensionAttributes']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); - $this->subscriberFactory->expects($this->any())->method('create')->willReturn($this->subscriber); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -105,7 +96,6 @@ protected function setUp() 'subscriberFactory' => $this->subscriberFactory, 'extensionFactory' => $this->extensionFactoryMock, 'subscriberResource' => $this->subscriberResourceMock, - 'storeManager' => $this->storeManagerMock, ] ); } @@ -216,7 +206,6 @@ public function testAfterGetByIdCreatesExtensionAttributesIfItIsNotSet( ) { $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $subscriber = [$subscriberStatusKey => $subscriberStatusValue]; - $this->prepareStoreData(); $this->extensionFactoryMock->expects($this->any()) ->method('create') @@ -244,7 +233,6 @@ public function testAfterGetByIdSetsIsSubscribedFlagIfItIsNotSet() { $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $subscriber = ['subscriber_id' => 1, 'subscriber_status' => 1]; - $this->prepareStoreData(); $this->customerMock->expects($this->any()) ->method('getExtensionAttributes') @@ -279,17 +267,4 @@ public function afterGetByIdDataProvider() [null, null, false], ]; } - - /** - * Prepare store information - * - * @return void - */ - private function prepareStoreData() - { - $storeId = 1; - $storeMock = $this->createMock(Store::class); - $storeMock->expects($this->any())->method('getId')->willReturn($storeId); - $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - } } diff --git a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml index 3eb7de194d242..5cc268333de71 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml @@ -15,6 +15,7 @@ <argument name="message_block_visibility" xsi:type="string">true</argument> <argument name="use_ajax" xsi:type="string">true</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> + <argument name="grid_url" xsi:type="url" path="*/*/grid"/> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.newslettrer.problem.grid.columnSet" as="grid.columnSet"> <arguments> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index a64185ce67958..532ecde456077 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -16,7 +16,16 @@ </div> <?php endif;?> </div> - <iframe name="preview_iframe" id="preview_iframe" frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%"></iframe> + <iframe + name="preview_iframe" + id="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock allow-scripts" + > + + </iframe> <?= $block->getChildHtml('preview_form') ?> </div> diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index c1a526ee95148..554437095f36c 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -19,16 +19,24 @@ data-mage-init='{"validation": {"errorClass": "mage-error"}}' id="newsletter-validate-detail"> <div class="field newsletter"> - <label class="label" for="newsletter"><span><?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?></span></label> <div class="control"> - <input name="email" type="email" id="newsletter" - placeholder="<?= $block->escapeHtml(__('Enter your email address')) ?>" - data-mage-init='{"mage/trim-input":{}}' - data-validate="{required:true, 'validate-email':true}"/> + <label for="newsletter"> + <span class="label"> + <?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?> + </span> + <input name="email" type="email" id="newsletter" + placeholder="<?= $block->escapeHtml(__('Enter your email address')) ?>" + data-mage-init='{"mage/trim-input":{}}' + data-validate="{required:true, 'validate-email':true}" + /> + </label> </div> </div> <div class="actions"> - <button class="action subscribe primary" title="<?= $block->escapeHtmlAttr(__('Subscribe')) ?>" type="submit"> + <button class="action subscribe primary sr-only" + title="<?= $block->escapeHtmlAttr(__('Subscribe')) ?>" + type="submit" + aria-label="Subscribe"> <span><?= $block->escapeHtml(__('Subscribe')) ?></span> </button> </div> diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..ec8dd46a00d8e --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,33 @@ +<?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="DisableCheckMoneyOrderPaymentMethod"> + <data key="path">payment/checkmo/active</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableCheckMoneyOrderPaymentMethod"> + <!-- Magento default value --> + <data key="path">payment/checkmo/active</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableCashOnDeliveryPaymentMethod"> + <!-- Magento default value --> + <data key="path">payment/cashondelivery/active</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableCashOnDeliveryPaymentMethod"> + <data key="path">payment/cashondelivery/active</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml new file mode 100644 index 0000000000000..4d63577319d5b --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @var $block \Magento\OfflinePayments\Block\Info\Checkmo + */ +?> +<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> + {{pdf_row_separator}} +<?php if ($block->getInfo()->getAdditionalInformation()): ?> + {{pdf_row_separator}} + <?php if ($block->getPayableTo()): ?> + <?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> + {{pdf_row_separator}} + <?php endif; ?> + <?php if ($block->getMailingAddress()): ?> + <?= $block->escapeHtml(__('Send Check to:')) ?> + {{pdf_row_separator}} + <?= /* @noEscape */ nl2br($block->escapeHtml($block->getMailingAddress())) ?> + {{pdf_row_separator}} + <?php endif; ?> +<?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml new file mode 100644 index 0000000000000..4a6ea1c00b21c --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** + * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder + */ +?> +<?= $block->escapeHtml(__('Purchase Order Number: %1', $block->getInfo()->getPoNumber())) ?> + {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 0000000000000..32810ecef20da --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout_billing"> + <arguments> + <argument name="form_templates" xsi:type="array"> + <item name="checkmo" xsi:type="string">Magento_OfflinePayments::multishipping/checkmo_form.phtml</item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml new file mode 100644 index 0000000000000..b96918243a7a7 --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'checkmo' + }; + layout([ + { + component: 'Magento_Checkout/js/view/payment/default', + name: 'payment_method_checkmo', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php index 26f3274688977..c2e6d0e922317 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php @@ -80,7 +80,7 @@ public function collectRates(RateRequest $request) $this->_updateFreeMethodQuote($request); - if ($request->getFreeShipping() || $request->getBaseSubtotalInclTax() >= $this->getConfigData( + if ($request->getFreeShipping() || $request->getPackageValueWithDiscount() >= $this->getConfigData( 'free_shipping_subtotal' ) ) { diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index bb81f9ebb475f..373d64afc8cc3 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -227,12 +227,12 @@ public function getCode($type, $code = '') $codes = [ 'condition_name' => [ 'package_weight' => __('Weight vs. Destination'), - 'package_value' => __('Price vs. Destination'), + 'package_value_with_discount' => __('Price vs. Destination'), 'package_qty' => __('# of Items vs. Destination'), ], 'condition_name_short' => [ 'package_weight' => __('Weight (and above)'), - 'package_value' => __('Order Subtotal (and above)'), + 'package_value_with_discount' => __('Order Subtotal (and above)'), 'package_qty' => __('# of Items (and above)'), ], ]; diff --git a/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php b/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php new file mode 100644 index 0000000000000..070105846fdd8 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OfflineShipping\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\OfflineShipping\Model\Carrier\Tablerate; + +/** + * Update for shipping_tablerate table for using price with discount in condition. + */ +class UpdateShippingTablerate implements DataPatchInterface +{ + /** + * @var \Magento\Framework\Setup\ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * PatchInitial constructor. + * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $connection = $this->moduleDataSetup->getConnection(); + $connection->update( + $this->moduleDataSetup->getTable('shipping_tablerate'), + ['condition_name' => 'package_value_with_discount'], + [new \Zend_Db_Expr('condition_name = \'package_value\'')] + ); + $connection->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => 'package_value_with_discount'], + [ + new \Zend_Db_Expr('value = \'package_value\''), + new \Zend_Db_Expr('path = \'carriers/tablerate/condition_name\'') + ] + ); + $this->moduleDataSetup->getConnection()->endSetup(); + + $connection->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php index 8d75cc32914b4..a1fb2e449d7bf 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php @@ -33,7 +33,10 @@ protected function setUp() $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import::class, - ['data' => $testData] + [ + 'data' => $testData, + '_escaper' => $testHelper->getObject(\Magento\Framework\Escaper::class) + ] ); $this->_object->setForm($this->_formMock); } diff --git a/app/code/Magento/OfflineShipping/etc/db_schema.xml b/app/code/Magento/OfflineShipping/etc/db_schema.xml index 0510ce9b9b8eb..5129e8a29b2a1 100644 --- a/app/code/Magento/OfflineShipping/etc/db_schema.xml +++ b/app/code/Magento/OfflineShipping/etc/db_schema.xml @@ -18,7 +18,7 @@ default="0" comment="Destination Region Id"/> <column xsi:type="varchar" name="dest_zip" nullable="false" length="10" default="*" comment="Destination Post Code (Zip)"/> - <column xsi:type="varchar" name="condition_name" nullable="false" length="20" comment="Rate Condition name"/> + <column xsi:type="varchar" name="condition_name" nullable="false" length="30" comment="Rate Condition name"/> <column xsi:type="decimal" name="condition_value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Rate condition value"/> <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" diff --git a/app/code/Magento/PageCache/Model/Cache/Server.php b/app/code/Magento/PageCache/Model/Cache/Server.php index 349e9faffa673..7f3a4af969d7e 100644 --- a/app/code/Magento/PageCache/Model/Cache/Server.php +++ b/app/code/Magento/PageCache/Model/Cache/Server.php @@ -12,6 +12,9 @@ use Zend\Uri\Uri; use Zend\Uri\UriFactory; +/** + * Cache server model. + */ class Server { /** @@ -62,8 +65,7 @@ public function getUris() foreach ($configuredHosts as $host) { $servers[] = UriFactory::factory('') ->setHost($host['host']) - ->setPort(isset($host['port']) ? $host['port'] : self::DEFAULT_PORT) - ; + ->setPort(isset($host['port']) ? $host['port'] : self::DEFAULT_PORT); } } elseif ($this->request->getHttpHost()) { $servers[] = UriFactory::factory('')->setHost($this->request->getHttpHost())->setPort(self::DEFAULT_PORT); diff --git a/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php b/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php index e16584b0b17f8..7c9391ba22182 100644 --- a/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php +++ b/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php @@ -28,7 +28,7 @@ public function beforeSave() throw new LocalizedException( new Phrase( 'Access List value "%1" is not valid. ' - .'Please use only IP addresses and host names.', + . 'Please use only IP addresses and host names.', [$value] ) ); diff --git a/app/code/Magento/PageCache/Model/Varnish/VclGenerator.php b/app/code/Magento/PageCache/Model/Varnish/VclGenerator.php index cf5a703142c84..a50fa090de2d8 100644 --- a/app/code/Magento/PageCache/Model/Varnish/VclGenerator.php +++ b/app/code/Magento/PageCache/Model/Varnish/VclGenerator.php @@ -9,6 +9,9 @@ use Magento\PageCache\Model\VclGeneratorInterface; use Magento\PageCache\Model\VclTemplateLocatorInterface; +/** + * Varnish vcl generator model. + */ class VclGenerator implements VclGeneratorInterface { /** @@ -119,7 +122,7 @@ private function getReplacements() private function getRegexForDesignExceptions() { $result = ''; - $tpl = "%s (req.http.user-agent ~ \"%s\") {\n"." hash_data(\"%s\");\n"." }"; + $tpl = "%s (req.http.user-agent ~ \"%s\") {\n" . " hash_data(\"%s\");\n" . " }"; $expressions = $this->getDesignExceptions(); @@ -143,7 +146,8 @@ private function getRegexForDesignExceptions() /** * Get IPs access list that can purge Varnish configuration for config file generation - * and transform it to appropriate view + * + * Tansform it to appropriate view * * acl purge{ * "127.0.0.1"; @@ -157,7 +161,7 @@ private function getTransformedAccessList() $result = array_reduce( $this->getAccessList(), function ($ips, $ip) use ($tpl) { - return $ips.sprintf($tpl, trim($ip)) . "\n"; + return $ips . sprintf($tpl, trim($ip)) . "\n"; }, '' ); @@ -216,6 +220,8 @@ private function getSslOffloadedHeader() } /** + * Get design exceptions array. + * * @return array */ private function getDesignExceptions() diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php new file mode 100644 index 0000000000000..7017da27eee93 --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php @@ -0,0 +1,108 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer; + +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\App\Cache\Manager; +use Magento\PageCache\Model\Cache\Type as PageCacheType; +use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance\PageCacheState; + +/** + * Switch Page Cache on maintenance. + */ +class SwitchPageCacheOnMaintenance implements ObserverInterface +{ + /** + * @var Manager + */ + private $cacheManager; + + /** + * @var PageCacheState + */ + private $pageCacheStateStorage; + + /** + * @param Manager $cacheManager + * @param PageCacheState $pageCacheStateStorage + */ + public function __construct(Manager $cacheManager, PageCacheState $pageCacheStateStorage) + { + $this->cacheManager = $cacheManager; + $this->pageCacheStateStorage = $pageCacheStateStorage; + } + + /** + * Switches Full Page Cache. + * + * Depending on enabling or disabling Maintenance Mode it turns off or restores Full Page Cache state. + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer): void + { + if ($observer->getData('isOn')) { + $this->pageCacheStateStorage->save($this->isFullPageCacheEnabled()); + $this->turnOffFullPageCache(); + } else { + $this->restoreFullPageCacheState(); + } + } + + /** + * Turns off Full Page Cache. + * + * @return void + */ + private function turnOffFullPageCache(): void + { + if (!$this->isFullPageCacheEnabled()) { + return; + } + + $this->cacheManager->clean([PageCacheType::TYPE_IDENTIFIER]); + $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], false); + } + + /** + * Full Page Cache state. + * + * @return bool + */ + private function isFullPageCacheEnabled(): bool + { + $cacheStatus = $this->cacheManager->getStatus(); + + if (!array_key_exists(PageCacheType::TYPE_IDENTIFIER, $cacheStatus)) { + return false; + } + + return (bool)$cacheStatus[PageCacheType::TYPE_IDENTIFIER]; + } + + /** + * Restores Full Page Cache state. + * + * Returns FPC to previous state that was before maintenance mode turning on. + * + * @return void + */ + private function restoreFullPageCacheState(): void + { + $storedPageCacheState = $this->pageCacheStateStorage->isEnabled(); + $this->pageCacheStateStorage->flush(); + + if ($storedPageCacheState) { + $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], true); + } + } +} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php new file mode 100644 index 0000000000000..e4cadf728f2ea --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -0,0 +1,74 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; + +use Magento\Framework\Filesystem; +use Magento\Framework\App\Filesystem\DirectoryList; + +/** + * Page Cache state. + */ +class PageCacheState +{ + /** + * Full Page Cache Off state file name. + */ + private const PAGE_CACHE_STATE_FILENAME = '.maintenance.fpc.state'; + + /** + * @var Filesystem\Directory\WriteInterface + */ + private $flagDir; + + /** + * @param Filesystem $fileSystem + */ + public function __construct(Filesystem $fileSystem) + { + $this->flagDir = $fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + } + + /** + * Saves Full Page Cache state. + * + * Saves FPC state across requests. + * + * @param bool $state + * @return void + */ + public function save(bool $state): void + { + $this->flagDir->writeFile(self::PAGE_CACHE_STATE_FILENAME, (string)$state); + } + + /** + * Returns stored Full Page Cache state. + * + * @return bool + */ + public function isEnabled(): bool + { + if (!$this->flagDir->isExist(self::PAGE_CACHE_STATE_FILENAME)) { + return false; + } + + return (bool)$this->flagDir->readFile(self::PAGE_CACHE_STATE_FILENAME); + } + + /** + * Flushes Page Cache state storage. + * + * @return void + */ + public function flush(): void + { + $this->flagDir->delete(self::PAGE_CACHE_STATE_FILENAME); + } +} diff --git a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml index 34a77095d524d..ee0c32633569a 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml @@ -30,5 +30,6 @@ <element name="pageCacheCheckbox" type="checkbox" selector="input[value='full_page']"/> <element name="webServicesConfigCheckbox" type="checkbox" selector="input[value='config_webservice']"/> <element name="translationsCheckbox" type="checkbox" selector="input[value='translate']"/> + <element name="additionalCacheButton" type="button" selector="//*[@id='container']//button[contains(., '{{cacheType}}')]" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml new file mode 100644 index 0000000000000..bd6f7ba362bf4 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -0,0 +1,35 @@ +<?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="FlushStaticFilesCacheButtonVisibilityTest"> + <annotations> + <features value="PageCache"/> + <stories value="Page Cache"/> + <title value="Check visibility of flush static files cache button"/> + <description value="Flush Static Files Cache button visibility"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15454"/> + <group value="production_mode_only"/> + <group value="pagecache"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Log in to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Open Cache Management page --> + <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="amOnPageCacheManagement"/> + <waitForPageLoad stepKey="waitForPageCacheManagementLoad"/> + + <!-- Check 'Flush Static Files Cache' not visible in production mode. --> + <dontSee selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton" /> + </test> +</tests> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/NewProductsListWidgetTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/NewProductsListWidgetTest.xml index c5871ddc3a373..a3c9e7b39217d 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/NewProductsListWidgetTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/NewProductsListWidgetTest.xml @@ -9,7 +9,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="NewProductsListWidgetSimpleProductTest"> - <actionGroup ref="clearPageCache" stepKey="clearPageCache" after="clickSaveProduct"/> + <actionGroup ref="clearPageCache" stepKey="clearPageCache" before="amOnCmsPage"/> </test> <test name="NewProductsListWidgetConfigurableProductTest"> <actionGroup ref="clearPageCache" stepKey="clearPageCache" after="clickSaveProduct"/> @@ -18,7 +18,7 @@ <actionGroup ref="clearPageCache" stepKey="clearPageCache" after="clickSaveProduct"/> </test> <test name="NewProductsListWidgetVirtualProductTest"> - <actionGroup ref="clearPageCache" stepKey="clearPageCache" after="clickSaveProduct"/> + <actionGroup ref="clearPageCache" stepKey="clearPageCache" before="amOnCmsPage"/> </test> <test name="NewProductsListWidgetBundleProductTest"> <actionGroup ref="clearPageCache" stepKey="clearPageCache" after="clickSaveProduct"/> diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php new file mode 100644 index 0000000000000..2dbb815c70925 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php @@ -0,0 +1,164 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Test\Unit\Observer; + +use PHPUnit\Framework\TestCase; +use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Cache\Manager; +use Magento\Framework\Event\Observer; +use Magento\PageCache\Model\Cache\Type as PageCacheType; +use Magento\PageCache\Observer\SwitchPageCacheOnMaintenance\PageCacheState; + +/** + * SwitchPageCacheOnMaintenance observer test. + */ +class SwitchPageCacheOnMaintenanceTest extends TestCase +{ + /** + * @var SwitchPageCacheOnMaintenance + */ + private $model; + + /** + * @var Manager|\PHPUnit\Framework\MockObject\MockObject + */ + private $cacheManager; + + /** + * @var PageCacheState|\PHPUnit\Framework\MockObject\MockObject + */ + private $pageCacheStateStorage; + + /** + * @var Observer|\PHPUnit\Framework\MockObject\MockObject + */ + private $observer; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->cacheManager = $this->createMock(Manager::class); + $this->pageCacheStateStorage = $this->createMock(PageCacheState::class); + $this->observer = $this->createMock(Observer::class); + + $this->model = $objectManager->getObject(SwitchPageCacheOnMaintenance::class, [ + 'cacheManager' => $this->cacheManager, + 'pageCacheStateStorage' => $this->pageCacheStateStorage, + ]); + } + + /** + * Tests execute when setting maintenance mode to on. + * + * @param array $cacheStatus + * @param bool $cacheState + * @param int $flushCacheCalls + * @return void + * @dataProvider enablingPageCacheStateProvider + */ + public function testExecuteWhileMaintenanceEnabling( + array $cacheStatus, + bool $cacheState, + int $flushCacheCalls + ): void { + $this->observer->method('getData') + ->with('isOn') + ->willReturn(true); + $this->cacheManager->method('getStatus') + ->willReturn($cacheStatus); + + // Page Cache state will be stored. + $this->pageCacheStateStorage->expects($this->once()) + ->method('save') + ->with($cacheState); + + // Page Cache will be cleaned and disabled + $this->cacheManager->expects($this->exactly($flushCacheCalls)) + ->method('clean') + ->with([PageCacheType::TYPE_IDENTIFIER]); + $this->cacheManager->expects($this->exactly($flushCacheCalls)) + ->method('setEnabled') + ->with([PageCacheType::TYPE_IDENTIFIER], false); + + $this->model->execute($this->observer); + } + + /** + * Tests execute when setting Maintenance Mode to off. + * + * @param bool $storedCacheState + * @param int $enableCacheCalls + * @return void + * @dataProvider disablingPageCacheStateProvider + */ + public function testExecuteWhileMaintenanceDisabling(bool $storedCacheState, int $enableCacheCalls): void + { + $this->observer->method('getData') + ->with('isOn') + ->willReturn(false); + + $this->pageCacheStateStorage->method('isEnabled') + ->willReturn($storedCacheState); + + // Nullify Page Cache state. + $this->pageCacheStateStorage->expects($this->once()) + ->method('flush'); + + // Page Cache will be enabled. + $this->cacheManager->expects($this->exactly($enableCacheCalls)) + ->method('setEnabled') + ->with([PageCacheType::TYPE_IDENTIFIER]); + + $this->model->execute($this->observer); + } + + /** + * Page Cache state data provider. + * + * @return array + */ + public function enablingPageCacheStateProvider(): array + { + return [ + 'page_cache_is_enable' => [ + 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 1], + 'cache_state' => true, + 'flush_cache_calls' => 1, + ], + 'page_cache_is_missing_in_system' => [ + 'cache_status' => [], + 'cache_state' => false, + 'flush_cache_calls' => 0, + ], + 'page_cache_is_disable' => [ + 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 0], + 'cache_state' => false, + 'flush_cache_calls' => 0, + ], + ]; + } + + /** + * Page Cache state data provider. + * + * @return array + */ + public function disablingPageCacheStateProvider(): array + { + return [ + ['stored_cache_state' => true, 'enable_cache_calls' => 1], + ['stored_cache_state' => false, 'enable_cache_calls' => 0], + ]; + } +} diff --git a/app/code/Magento/PageCache/etc/events.xml b/app/code/Magento/PageCache/etc/events.xml index 7584f5f36d69c..3f0a2532ae60a 100644 --- a/app/code/Magento/PageCache/etc/events.xml +++ b/app/code/Magento/PageCache/etc/events.xml @@ -57,4 +57,7 @@ <event name="customer_logout"> <observer name="FlushFormKey" instance="Magento\PageCache\Observer\FlushFormKey"/> </event> + <event name="maintenance_mode_changed"> + <observer name="page_cache_switcher_for_maintenance" instance="Magento\PageCache\Observer\SwitchPageCacheOnMaintenance"/> + </event> </config> diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index 8068447e5ca99..801e6cb475d8a 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -91,10 +91,11 @@ sub vcl_recv { } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 6c8414a5cb641..76c5ffee5f14f 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -92,10 +92,11 @@ sub vcl_recv { } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { diff --git a/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php new file mode 100644 index 0000000000000..8afa064efd3ea --- /dev/null +++ b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Api\Data; + +use Magento\Framework\DataObject\KeyValueObjectInterface; + +/** + * Payment additional info interface. + */ +interface PaymentAdditionalInfoInterface extends KeyValueObjectInterface +{ +} diff --git a/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php b/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php index f1a8950514152..110fe10ee5c3b 100644 --- a/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php +++ b/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Payment\Gateway\Validator; /** - * Class AbstractValidator - * @package Magento\Payment\Gateway\Validator + * Represents a basic validator shell that can create a result + * * @api * @since 100.0.2 */ @@ -33,7 +36,7 @@ public function __construct( * @param bool $isValid * @param array $fails * @param array $errorCodes - * @return void + * @return \Magento\Payment\Gateway\Validator\ResultInterface */ protected function createResult($isValid, array $fails = [], array $errorCodes = []) { diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index b7f1368ddabce..8ea97d31ed4d9 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -10,10 +10,9 @@ use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; /** - * Class ValidatorComposite - * @package Magento\Payment\Gateway\Validator + * Compiles a result using the results of multiple validators + * * @api - * @since 100.0.2 */ class ValidatorComposite extends AbstractValidator { @@ -22,15 +21,22 @@ class ValidatorComposite extends AbstractValidator */ private $validators; + /** + * @var array + */ + private $chainBreakingValidators; + /** * @param ResultInterfaceFactory $resultFactory * @param TMapFactory $tmapFactory * @param array $validators + * @param array $chainBreakingValidators */ public function __construct( ResultInterfaceFactory $resultFactory, TMapFactory $tmapFactory, - array $validators = [] + array $validators = [], + array $chainBreakingValidators = [] ) { $this->validators = $tmapFactory->create( [ @@ -38,6 +44,7 @@ public function __construct( 'type' => ValidatorInterface::class ] ); + $this->chainBreakingValidators = $chainBreakingValidators; parent::__construct($resultFactory); } @@ -51,7 +58,8 @@ public function validate(array $validationSubject) { $isValid = true; $failsDescriptionAggregate = []; - foreach ($this->validators as $validator) { + $errorCodesAggregate = []; + foreach ($this->validators as $key => $validator) { $result = $validator->validate($validationSubject); if (!$result->isValid()) { $isValid = false; @@ -59,9 +67,16 @@ public function validate(array $validationSubject) $failsDescriptionAggregate, $result->getFailsDescription() ); + $errorCodesAggregate = array_merge( + $errorCodesAggregate, + $result->getErrorCodes() + ); + if (!empty($this->chainBreakingValidators[$key])) { + break; + } } } - return $this->createResult($isValid, $failsDescriptionAggregate); + return $this->createResult($isValid, $failsDescriptionAggregate, $errorCodesAggregate); } } diff --git a/app/code/Magento/Payment/Helper/Data.php b/app/code/Magento/Payment/Helper/Data.php index 0a4990313fa82..9bea19700d452 100644 --- a/app/code/Magento/Payment/Helper/Data.php +++ b/app/code/Magento/Payment/Helper/Data.php @@ -84,6 +84,8 @@ public function __construct( } /** + * Get config name of method model + * * @param string $code * @return string */ @@ -259,10 +261,13 @@ public function getPaymentMethodList($sorted = true, $asLabelValue = false, $wit $groupRelations = []; foreach ($this->getPaymentMethods() as $code => $data) { - if (isset($data['title'])) { - $methods[$code] = $data['title']; - } else { - $methods[$code] = $this->getMethodInstance($code)->getConfigData('title', $store); + if (!empty($data['active'])) { + $storedTitle = $this->getMethodInstance($code)->getConfigData('title', $store); + if (isset($storedTitle)) { + $methods[$code] = $storedTitle; + } elseif (isset($data['title'])) { + $methods[$code] = $data['title']; + } } if ($asLabelValue && $withGroups && isset($data['group'])) { $groupRelations[$code] = $data['group']; diff --git a/app/code/Magento/Payment/Model/CcConfigProvider.php b/app/code/Magento/Payment/Model/CcConfigProvider.php index 15bdd0072a51a..497ce93c30c71 100644 --- a/app/code/Magento/Payment/Model/CcConfigProvider.php +++ b/app/code/Magento/Payment/Model/CcConfigProvider.php @@ -44,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { @@ -69,7 +69,7 @@ public function getIcons() } $types = $this->ccConfig->getCcAvailableTypes(); - foreach (array_keys($types) as $code) { + foreach ($types as $code => $label) { if (!array_key_exists($code, $this->icons)) { $asset = $this->ccConfig->createAsset('Magento_Payment::images/cc/' . strtolower($code) . '.png'); $placeholder = $this->assetSource->findSource($asset); @@ -78,7 +78,8 @@ public function getIcons() $this->icons[$code] = [ 'url' => $asset->getUrl(), 'width' => $width, - 'height' => $height + 'height' => $height, + 'title' => __($label), ]; } } diff --git a/app/code/Magento/Payment/Model/Method/Cc.php b/app/code/Magento/Payment/Model/Method/Cc.php index c23ad5b535dd8..11629308cd46b 100644 --- a/app/code/Magento/Payment/Model/Method/Cc.php +++ b/app/code/Magento/Payment/Model/Method/Cc.php @@ -10,6 +10,8 @@ use Magento\Quote\Model\Quote\Payment; /** + * Credit Card payment method legacy implementation. + * * @method \Magento\Quote\Api\Data\PaymentMethodExtensionInterface getExtensionAttributes() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated 100.0.8 @@ -93,6 +95,7 @@ public function __construct( * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function validate() { @@ -148,6 +151,22 @@ public function validate() 'JCB' => '/^35(2[8-9][0-9]{12,15}|[3-8][0-9]{13,16})/', 'MI' => '/^(5(0|[6-9])|63|67(?!59|6770|6774))\d*$/', 'MD' => '/^(6759(?!24|38|40|6[3-9]|70|76)|676770|676774)\d*$/', + + //Hipercard + 'HC' => '/^((606282)|(637095)|(637568)|(637599)|(637609)|(637612))\d*$/', + //Elo + 'ELO' => '/^((509091)|(636368)|(636297)|(504175)|(438935)|(40117[8-9])|(45763[1-2])|' . + '(457393)|(431274)|(50990[0-2])|(5099[7-9][0-9])|(50996[4-9])|(509[1-8][0-9][0-9])|' . + '(5090(0[0-2]|0[4-9]|1[2-9]|[24589][0-9]|3[1-9]|6[0-46-9]|7[0-24-9]))|' . + '(5067(0[0-24-8]|1[0-24-9]|2[014-9]|3[0-379]|4[0-9]|5[0-3]|6[0-5]|7[0-8]))|' . + '(6504(0[5-9]|1[0-9]|2[0-9]|3[0-9]))|' . + '(6504(8[5-9]|9[0-9])|6505(0[0-9]|1[0-9]|2[0-9]|3[0-8]))|' . + '(6505(4[1-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-8]))|' . + '(6507(0[0-9]|1[0-8]))|(65072[0-7])|(6509(0[1-9]|1[0-9]|20))|' . + '(6516(5[2-9]|6[0-9]|7[0-9]))|(6550(0[0-9]|1[0-9]))|' . + '(6550(2[1-9]|3[0-9]|4[0-9]|5[0-8])))\d*$/', + //Aura + 'AU' => '/^5078\d*$/' ]; $ccNumAndTypeMatches = isset( @@ -189,6 +208,8 @@ public function validate() } /** + * Check if verification should be used. + * * @return bool * @api */ @@ -202,6 +223,8 @@ public function hasVerification() } /** + * Get list of credit cards verification reg exp. + * * @return array * @api */ @@ -226,6 +249,8 @@ public function getVerificationRegEx() } /** + * Validate expiration date + * * @param string $expYear * @param string $expMonth * @return bool @@ -276,6 +301,8 @@ public function assignData(\Magento\Framework\DataObject $data) } /** + * Get code for "other" credit cards. + * * @param string $type * @return bool * @api diff --git a/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php new file mode 100644 index 0000000000000..c4f135d5e0044 --- /dev/null +++ b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Model; + +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; + +/** + * Payment additional info class. + */ +class PaymentAdditionalInfo implements PaymentAdditionalInfoInterface +{ + /** + * @var string + */ + private $key; + + /** + * @var string + */ + private $value; + + /** + * @inheritdoc + */ + public function getKey() + { + return $this->key; + } + + /** + * @inheritdoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritdoc + */ + public function setKey($key) + { + $this->key = $key; + return $key; + } + + /** + * @inheritdoc + */ + public function setValue($value) + { + $this->value = $value; + return $value; + } +} diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php index 7352cb7a4ac6d..5dec99e2a4b1b 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php @@ -13,9 +13,9 @@ class ValidatorCompositeTest extends \PHPUnit\Framework\TestCase public function testValidate() { $validationSubject = []; - $validator1 = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ValidatorInterface::class) + $validator1 = $this->getMockBuilder(ValidatorInterface::class) ->getMockForAbstractClass(); - $validator2 = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ValidatorInterface::class) + $validator2 = $this->getMockBuilder(ValidatorInterface::class) ->getMockForAbstractClass(); $tMapFactory = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMapFactory::class) ->disableOriginalConstructor() @@ -30,8 +30,8 @@ public function testValidate() ->with( [ 'array' => [ - 'validator1' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class, - 'validator2' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class ], 'type' => ValidatorInterface::class ] @@ -54,6 +54,9 @@ public function testValidate() $resultFail->expects(static::once()) ->method('getFailsDescription') ->willReturn(['Fail']); + $resultFail->expects(static::once()) + ->method('getErrorCodes') + ->willReturn(['abc123']); $validator1->expects(static::once()) ->method('validate') @@ -76,7 +79,7 @@ public function testValidate() [ 'isValid' => false, 'failsDescription' => ['Fail'], - 'errorCodes' => [] + 'errorCodes' => ['abc123'] ] ) ->willReturn($compositeResult); @@ -85,10 +88,91 @@ public function testValidate() $resultFactory, $tMapFactory, [ - 'validator1' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class, - 'validator2' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class ] ); static::assertSame($compositeResult, $validatorComposite->validate($validationSubject)); } + + public function testValidateChainBreaksCorrectly() + { + $validationSubject = []; + $validator1 = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $validator2 = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $tMapFactory = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMapFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $tMap = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMap::class) + ->disableOriginalConstructor() + ->getMock(); + + $tMapFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'array' => [ + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class + ], + 'type' => ValidatorInterface::class + ] + ) + ->willReturn($tMap); + $tMap->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$validator1, $validator2])); + + $resultFail = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + ->getMockForAbstractClass(); + $resultFail->expects($this->once()) + ->method('isValid') + ->willReturn(false); + $resultFail->expects($this->once()) + ->method('getFailsDescription') + ->willReturn(['Fail']); + $resultFail->expects($this->once()) + ->method('getErrorCodes') + ->willReturn(['abc123']); + + $validator1->expects($this->once()) + ->method('validate') + ->with($validationSubject) + ->willReturn($resultFail); + + // Assert this is never called + $validator2->expects($this->never()) + ->method('validate'); + + $compositeResult = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + ->getMockForAbstractClass(); + $resultFactory = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $resultFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'isValid' => false, + 'failsDescription' => ['Fail'], + 'errorCodes' => ['abc123'] + ] + ) + ->willReturn($compositeResult); + + $validatorComposite = new ValidatorComposite( + $resultFactory, + $tMapFactory, + [ + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class + ], + ['validator1'] + ); + $this->assertSame($compositeResult, $validatorComposite->validate($validationSubject)); + } } diff --git a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php index a8856166995fc..ff6aea44645cf 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php @@ -42,12 +42,14 @@ public function testGetConfig() 'vi' => [ 'url' => 'http://cc.card/vi.png', 'width' => getimagesize($imagesDirectoryPath . 'vi.png')[0], - 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1] + 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1], + 'title' => __('Visa'), ], 'ae' => [ 'url' => 'http://cc.card/ae.png', 'width' => getimagesize($imagesDirectoryPath . 'ae.png')[0], - 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1] + 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1], + 'title' => __('American Express'), ] ] ] @@ -56,11 +58,13 @@ public function testGetConfig() $ccAvailableTypesMock = [ 'vi' => [ + 'title' => 'Visa', 'fileId' => 'Magento_Payment::images/cc/vi.png', 'path' => $imagesDirectoryPath . 'vi.png', 'url' => 'http://cc.card/vi.png' ], 'ae' => [ + 'title' => 'American Express', 'fileId' => 'Magento_Payment::images/cc/ae.png', 'path' => $imagesDirectoryPath . 'ae.png', 'url' => 'http://cc.card/ae.png' @@ -68,7 +72,11 @@ public function testGetConfig() ]; $assetMock = $this->createMock(\Magento\Framework\View\Asset\File::class); - $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes')->willReturn($ccAvailableTypesMock); + $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes') + ->willReturn(array_combine( + array_keys($ccAvailableTypesMock), + array_column($ccAvailableTypesMock, 'title') + )); $this->ccConfigMock->expects($this->atLeastOnce()) ->method('createAsset') diff --git a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php index 5afaa9fcf97b9..fbf80de519f9f 100644 --- a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php +++ b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php @@ -25,8 +25,9 @@ class Options implements \Magento\Framework\Data\OptionSourceInterface * * @param \Magento\Payment\Helper\Data $paymentHelper */ - public function __construct(\Magento\Payment\Helper\Data $paymentHelper) - { + public function __construct( + \Magento\Payment\Helper\Data $paymentHelper + ) { $this->paymentHelper = $paymentHelper; } diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml index 74f553cc64094..b7422bb00d543 100644 --- a/app/code/Magento/Payment/etc/di.xml +++ b/app/code/Magento/Payment/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Payment\Api\Data\PaymentMethodInterface" type="Magento\Payment\Model\PaymentMethod"/> + <preference for="Magento\Payment\Api\Data\PaymentAdditionalInfoInterface" type="Magento\Payment\Model\PaymentAdditionalInfo"/> <preference for="Magento\Payment\Api\PaymentMethodListInterface" type="Magento\Payment\Model\PaymentMethodList"/> <preference for="Magento\Payment\Gateway\Validator\ResultInterface" type="Magento\Payment\Gateway\Validator\Result"/> <preference for="Magento\Payment\Gateway\ConfigFactoryInterface" type="Magento\Payment\Gateway\Config\ConfigFactory" /> diff --git a/app/code/Magento/Payment/etc/payment.xml b/app/code/Magento/Payment/etc/payment.xml index 19b5eb709c649..4afb6b01b366c 100644 --- a/app/code/Magento/Payment/etc/payment.xml +++ b/app/code/Magento/Payment/etc/payment.xml @@ -41,5 +41,14 @@ <type id="MD" order="100"> <label>Maestro Domestic</label> </type> + <type id="HC" order="110"> + <label>Hipercard</label> + </type> + <type id="ELO" order="120"> + <label>Elo</label> + </type> + <type id="AU" order="130"> + <label>Aura</label> + </type> </credit_cards> </payment> diff --git a/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml new file mode 100644 index 0000000000000..7acac62f65d38 --- /dev/null +++ b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @see \Magento\Payment\Block\Info + * @var \Magento\Payment\Block\Info $block + */ +?> +<?= $block->escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} + +<?php if ($specificInfo = $block->getSpecificInformation()):?> + <?php foreach ($specificInfo as $label => $value):?> + <?= $block->escapeHtml($label) ?>: + <?= $block->escapeHtml(implode(' ', $block->getValueAsArray($value))) ?> + {{pdf_row_separator}} + <?php endforeach; ?> +<?php endif;?> + +<?= $block->escapeHtml(implode('{{pdf_row_separator}}', $block->getChildPdfAsArray())) ?> diff --git a/app/code/Magento/Payment/view/base/web/images/cc/au.png b/app/code/Magento/Payment/view/base/web/images/cc/au.png new file mode 100644 index 0000000000000..04cb2df8fa332 Binary files /dev/null and b/app/code/Magento/Payment/view/base/web/images/cc/au.png differ diff --git a/app/code/Magento/Payment/view/base/web/images/cc/elo.png b/app/code/Magento/Payment/view/base/web/images/cc/elo.png new file mode 100644 index 0000000000000..eba0296a09104 Binary files /dev/null and b/app/code/Magento/Payment/view/base/web/images/cc/elo.png differ diff --git a/app/code/Magento/Payment/view/base/web/images/cc/hc.png b/app/code/Magento/Payment/view/base/web/images/cc/hc.png new file mode 100644 index 0000000000000..203e0b7e305c1 Binary files /dev/null and b/app/code/Magento/Payment/view/base/web/images/cc/hc.png differ diff --git a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js index 3ac67f6f31002..1b387b384104f 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js @@ -110,6 +110,48 @@ define([ name: 'CVC', size: 3 } + }, + { + title: 'Hipercard', + type: 'HC', + pattern: '^((606282)|(637095)|(637568)|(637599)|(637609)|(637612))\\d*$', + gaps: [4, 8, 12], + lengths: [13, 16], + code: { + name: 'CVC', + size: 3 + } + }, + { + title: 'Elo', + type: 'ELO', + pattern: '^((509091)|(636368)|(636297)|(504175)|(438935)|(40117[8-9])|(45763[1-2])|' + + '(457393)|(431274)|(50990[0-2])|(5099[7-9][0-9])|(50996[4-9])|(509[1-8][0-9][0-9])|' + + '(5090(0[0-2]|0[4-9]|1[2-9]|[24589][0-9]|3[1-9]|6[0-46-9]|7[0-24-9]))|' + + '(5067(0[0-24-8]|1[0-24-9]|2[014-9]|3[0-379]|4[0-9]|5[0-3]|6[0-5]|7[0-8]))|' + + '(6504(0[5-9]|1[0-9]|2[0-9]|3[0-9]))|' + + '(6504(8[5-9]|9[0-9])|6505(0[0-9]|1[0-9]|2[0-9]|3[0-8]))|' + + '(6505(4[1-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-8]))|' + + '(6507(0[0-9]|1[0-8]))|(65072[0-7])|(6509(0[1-9]|1[0-9]|20))|' + + '(6516(5[2-9]|6[0-9]|7[0-9]))|(6550(0[0-9]|1[0-9]))|' + + '(6550(2[1-9]|3[0-9]|4[0-9]|5[0-8])))\\d*$', + gaps: [4, 8, 12], + lengths: [16], + code: { + name: 'CVC', + size: 3 + } + }, + { + title: 'Aura', + type: 'AU', + pattern: '^5078\\d*$', + gaps: [4, 8, 12], + lengths: [19], + code: { + name: 'CVC', + size: 3 + } } ]; diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml index afa71fe591495..ea06c60c30e20 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -40,7 +40,7 @@ $params = $block->getParams(); $(parent).trigger('clearTimeout'); fullScreenLoader.stopLoader(); globalMessageList.addErrorMessage({ - message: $t(<?= /* @escapeNotVerified */ json_encode($params['error_msg'])?>) + message: $t(<?= /* @noEscape */ json_encode($params['error_msg'])?>) }); } ); diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Depends/ButtonStylesLabel.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Depends/ButtonStylesLabel.php new file mode 100644 index 0000000000000..82e0e55660638 --- /dev/null +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Depends/ButtonStylesLabel.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends; + +use Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable; + +/** + * Class ButtonStylesLabel + */ +class ButtonStylesLabel extends AbstractEnable +{ + /** + * Getting the name of a UI attribute + * + * @return string + */ + protected function getDataAttributeName() + { + return 'button-label'; + } +} diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php index 1a8a5895c434c..88a33f19de2f4 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php @@ -7,6 +7,8 @@ /** * Class Bml + * @deprecated + * "Enable PayPal Credit" setting was removed. Please @see "Disable Funding Options" */ class BmlApi extends AbstractEnable { diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php new file mode 100644 index 0000000000000..bad4dad4c0955 --- /dev/null +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/MultiSelect/DisabledFundingOptions.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Block\Adminhtml\System\Config\MultiSelect; + +use Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable; +use Magento\Paypal\Model\Config\StructurePlugin; +use Magento\Backend\Block\Template\Context; +use Magento\Paypal\Model\Config; +use Magento\Framework\Data\Form\Element\AbstractElement; + +/** + * Class DisabledFundingOptions + */ +class DisabledFundingOptions extends AbstractEnable +{ + /** + * @var Config + */ + private $config; + + /** + * DisabledFundingOptions constructor. + * @param Context $context + * @param Config $config + * @param array $data + */ + public function __construct( + Context $context, + Config $config, + $data = [] + ) { + $this->config = $config; + parent::__construct($context, $data); + } + + /** + * Render country field considering request parameter + * + * @param AbstractElement $element + * @return string + */ + public function render(AbstractElement $element) + { + if (!$this->isSelectedMerchantCountry('US')) { + $fundingOptions = $element->getValues(); + $element->setValues($this->filterValuesForPaypalCredit($fundingOptions)); + } + return parent::render($element); + } + + /** + * Getting the name of a UI attribute + * + * @return string + */ + protected function getDataAttributeName(): string + { + return 'disable-funding-options'; + } + + /** + * Filters array for CREDIT + * + * @param array $options + * @return array + */ + private function filterValuesForPaypalCredit($options): array + { + return array_filter($options, function ($opt) { + return ($opt['value'] !== 'CREDIT'); + }); + } + + /** + * Checks for chosen Merchant country from the config/url + * + * @param string $country + * @return bool + */ + private function isSelectedMerchantCountry(string $country): bool + { + $merchantCountry = $this->getRequest()->getParam(StructurePlugin::REQUEST_PARAM_COUNTRY) + ?: $this->config->getMerchantCountry(); + return $merchantCountry === $country; + } +} diff --git a/app/code/Magento/Paypal/Block/Bml/Shortcut.php b/app/code/Magento/Paypal/Block/Bml/Shortcut.php index 39e5dbd3cefce..d2f5ca009a198 100644 --- a/app/code/Magento/Paypal/Block/Bml/Shortcut.php +++ b/app/code/Magento/Paypal/Block/Bml/Shortcut.php @@ -8,7 +8,13 @@ use Magento\Catalog\Block as CatalogBlock; use Magento\Paypal\Helper\Shortcut\ValidatorInterface; +use Magento\Paypal\Model\ConfigFactory; +use Magento\Paypal\Model\Config; +use Magento\Framework\App\ObjectManager; +/** + * Class shortcut + */ class Shortcut extends \Magento\Framework\View\Element\Template implements CatalogBlock\ShortcutInterface { /** @@ -66,6 +72,11 @@ class Shortcut extends \Magento\Framework\View\Element\Template implements Catal */ private $_shortcutValidator; + /** + * @var Config + */ + private $config; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Payment\Helper\Data $paymentData @@ -77,7 +88,9 @@ class Shortcut extends \Magento\Framework\View\Element\Template implements Catal * @param string $bmlMethodCode * @param string $shortcutTemplate * @param array $data + * @param ConfigFactory|null $config * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @codingStandardsIgnoreStart */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -89,28 +102,35 @@ public function __construct( $alias, $bmlMethodCode, $shortcutTemplate, - array $data = [] + array $data = [], + ConfigFactory $config = null ) { $this->_paymentData = $paymentData; $this->_mathRandom = $mathRandom; $this->_shortcutValidator = $shortcutValidator; - $this->_paymentMethodCode = $paymentMethodCode; $this->_startAction = $startAction; $this->_alias = $alias; $this->setTemplate($shortcutTemplate); $this->_bmlMethodCode = $bmlMethodCode; + $this->config = $config + ? $config->create() + : ObjectManager::getInstance()->get(ConfigFactory::class)->create(); + $this->config->setMethod($this->_paymentMethodCode); parent::__construct($context, $data); } + //@codingStandardsIgnoreEnd /** - * @return \Magento\Framework\View\Element\AbstractBlock + * @inheritdoc */ protected function _beforeToHtml() { $result = parent::_beforeToHtml(); $isInCatalog = $this->getIsInCatalogProduct(); - if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog)) { + if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog) + || (bool)(int)$this->config->getValue('in_context') + ) { $this->_shouldRender = false; return $result; } diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php index 79142ecb1bfad..8d1e04c1397fc 100644 --- a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php +++ b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php @@ -9,7 +9,6 @@ use Magento\Payment\Model\MethodInterface; use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; -use Magento\Paypal\Block\Express\InContext; use Magento\Framework\View\Element\Template; use Magento\Catalog\Block\ShortcutInterface; use Magento\Framework\Locale\ResolverInterface; @@ -17,6 +16,7 @@ /** * Class Button + * @deprecated @see \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton */ class Button extends Template implements ShortcutInterface { @@ -59,8 +59,8 @@ class Button extends Template implements ShortcutInterface * @param Context $context * @param ResolverInterface $localeResolver * @param ConfigFactory $configFactory - * @param MethodInterface $payment * @param Session $session + * @param MethodInterface $payment * @param array $data */ public function __construct( @@ -101,8 +101,7 @@ private function isVisibleOnCart() } /** - * Check is Paypal In-Context Express Checkout button - * should render in cart/mini-cart + * Check is Paypal In-Context Express Checkout button should render in cart/mini-cart * * @return bool */ @@ -127,6 +126,8 @@ protected function _toHtml() } /** + * Returns container id + * * @return string */ public function getContainerId() @@ -135,6 +136,8 @@ public function getContainerId() } /** + * Returns link action + * * @return string */ public function getLinkAction() @@ -143,6 +146,8 @@ public function getLinkAction() } /** + * Returns add to cart selector + * * @return string */ public function getAddToCartSelector() @@ -151,6 +156,8 @@ public function getAddToCartSelector() } /** + * Returns image url + * * @return string */ public function getImageUrl() @@ -171,6 +178,8 @@ public function getAlias() } /** + * Set information if button renders in the mini cart + * * @param bool $isCatalog * @return $this */ diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/SmartButton.php b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/SmartButton.php new file mode 100644 index 0000000000000..c6a17fa5efb9f --- /dev/null +++ b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/SmartButton.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Block\Express\InContext\Minicart; + +use Magento\Checkout\Model\Session; +use Magento\Payment\Model\MethodInterface; +use Magento\Paypal\Model\Config; +use Magento\Paypal\Model\ConfigFactory; +use Magento\Framework\View\Element\Template; +use Magento\Catalog\Block\ShortcutInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Paypal\Model\SmartButtonConfig; +use Magento\Framework\UrlInterface; +use Magento\Quote\Model\QuoteIdToMaskedQuoteId; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Class Button + */ +class SmartButton extends Template implements ShortcutInterface +{ + private const ALIAS_ELEMENT_INDEX = 'alias'; + + /** + * @var Config + */ + private $config; + + /** + * @var MethodInterface + */ + private $payment; + + /** + * @var Session + */ + private $session; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var SmartButtonConfig + */ + private $smartButtonConfig; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @var QuoteIdToMaskedQuoteId + */ + private $quoteIdMask; + + /** + * @param Context $context + * @param ConfigFactory $configFactory + * @param Session $session + * @param MethodInterface $payment + * @param SerializerInterface $serializer + * @param SmartButtonConfig $smartButtonConfig + * @param UrlInterface $urlBuilder + * @param QuoteIdToMaskedQuoteId $quoteIdToMaskedQuoteId + * @param array $data + */ + public function __construct( + Context $context, + ConfigFactory $configFactory, + Session $session, + MethodInterface $payment, + SerializerInterface $serializer, + SmartButtonConfig $smartButtonConfig, + UrlInterface $urlBuilder, + QuoteIdToMaskedQuoteId $quoteIdToMaskedQuoteId, + array $data = [] + ) { + parent::__construct($context, $data); + + $this->config = $configFactory->create(); + $this->config->setMethod(Config::METHOD_EXPRESS); + $this->payment = $payment; + $this->session = $session; + $this->serializer = $serializer; + $this->smartButtonConfig = $smartButtonConfig; + $this->urlBuilder = $urlBuilder; + $this->quoteIdMask = $quoteIdToMaskedQuoteId; + } + + /** + * Check `in_context` config value + * + * @return bool + */ + private function isInContext(): bool + { + return (bool)(int) $this->config->getValue('in_context'); + } + + /** + * Check `visible_on_cart` config value + * + * @return bool + */ + private function isVisibleOnCart(): bool + { + return (bool)(int) $this->config->getValue('visible_on_cart'); + } + + /** + * Check is Paypal In-Context Express Checkout button should render in cart/mini-cart + * + * @return bool + */ + private function shouldRender(): bool + { + return $this->payment->isAvailable($this->session->getQuote()) + && $this->isInContext() + && $this->isVisibleOnCart() + && $this->getQuoteId() + && !$this->getIsInCatalogProduct(); + } + + /** + * @inheritdoc + */ + protected function _toHtml() + { + if (!$this->shouldRender()) { + return ''; + } + + return parent::_toHtml(); + } + + /** + * Get shortcut alias + * + * @return string + */ + public function getAlias() + { + return $this->getData(self::ALIAS_ELEMENT_INDEX); + } + + /** + * Returns string to initialize js component + * + * @return string + */ + public function getJsInitParams(): string + { + $config = []; + $quoteId = $this->getQuoteId(); + if (!empty($quoteId)) { + $clientConfig = [ + 'quoteId' => $quoteId, + 'customerId' => $this->session->getQuote()->getCustomerId(), + 'button' => 1, + 'getTokenUrl' => $this->urlBuilder->getUrl( + 'paypal/express/getTokenData', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onAuthorizeUrl' => $this->urlBuilder->getUrl( + 'paypal/express/onAuthorization', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onCancelUrl' => $this->urlBuilder->getUrl( + 'paypal/express/cancel', + ['_secure' => $this->getRequest()->isSecure()] + ) + ]; + $smartButtonsConfig = $this->getIsShoppingCart() + ? $this->smartButtonConfig->getConfig('cart') + : $this->smartButtonConfig->getConfig('mini_cart'); + $clientConfig = array_replace_recursive($clientConfig, $smartButtonsConfig); + $config = [ + 'Magento_Paypal/js/in-context/button' => [ + 'clientConfig' => $clientConfig + ] + ]; + } + $json = $this->serializer->serialize($config); + return $json; + } + + /** + * Returns container id + * + * @return string + */ + public function getContainerId(): string + { + return $this->getData('button_id'); + } + + /** + * Get quote id from session + * + * @return string + */ + private function getQuoteId(): string + { + $quoteId = (int)$this->session->getQuoteId(); + if (!$this->session->getQuote()->getCustomerId()) { + try { + $quoteId = $this->quoteIdMask->execute($quoteId); + } catch (NoSuchEntityException $e) { + $quoteId = ""; + } + } + return (string)$quoteId; + } +} diff --git a/app/code/Magento/Paypal/Block/Express/InContext/SmartButton.php b/app/code/Magento/Paypal/Block/Express/InContext/SmartButton.php new file mode 100644 index 0000000000000..6d355038cff1f --- /dev/null +++ b/app/code/Magento/Paypal/Block/Express/InContext/SmartButton.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Block\Express\InContext; + +use Magento\Paypal\Model\Config; +use Magento\Paypal\Model\ConfigFactory; +use Magento\Framework\View\Element\Template; +use Magento\Catalog\Block\ShortcutInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Paypal\Model\SmartButtonConfig; +use Magento\Framework\UrlInterface; + +/** + * Class Button + */ +class SmartButton extends Template implements ShortcutInterface +{ + private const ALIAS_ELEMENT_INDEX = 'alias'; + + /** + * @var Config + */ + private $config; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var SmartButtonConfig + */ + private $smartButtonConfig; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param Context $context + * @param ConfigFactory $configFactory + * @param SerializerInterface $serializer + * @param SmartButtonConfig $smartButtonConfig + * @param UrlInterface $urlBuilder + * @param array $data + */ + public function __construct( + Context $context, + ConfigFactory $configFactory, + SerializerInterface $serializer, + SmartButtonConfig $smartButtonConfig, + UrlInterface $urlBuilder, + array $data = [] + ) { + parent::__construct($context, $data); + + $this->config = $configFactory->create(); + $this->config->setMethod(Config::METHOD_EXPRESS); + $this->serializer = $serializer; + $this->smartButtonConfig = $smartButtonConfig; + $this->urlBuilder = $urlBuilder; + } + + /** + * Check is Paypal In-Context Express Checkout button should render in cart/mini-cart + * + * @return bool + */ + private function shouldRender(): bool + { + $isInCatalog = $this->getIsInCatalogProduct(); + $isInContext = (bool)(int) $this->config->getValue('in_context'); + + return ($isInContext && $isInCatalog); + } + + /** + * @inheritdoc + */ + protected function _toHtml() + { + if (!$this->shouldRender()) { + return ''; + } + + return parent::_toHtml(); + } + + /** + * Get shortcut alias + * + * @return string + */ + public function getAlias() + { + return $this->getData(self::ALIAS_ELEMENT_INDEX); + } + + /** + * Returns string to initialize js component + * + * @return string + */ + public function getJsInitParams(): string + { + $clientConfig = [ + 'button' => 1, + 'getTokenUrl' => $this->urlBuilder->getUrl( + 'paypal/express/getTokenData', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onAuthorizeUrl' => $this->urlBuilder->getUrl( + 'paypal/express/onAuthorization', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onCancelUrl' => $this->urlBuilder->getUrl( + 'paypal/express/cancel', + ['_secure' => $this->getRequest()->isSecure()] + ) + ]; + $smartButtonsConfig = $this->smartButtonConfig->getConfig('product'); + $clientConfig = array_replace_recursive($clientConfig, $smartButtonsConfig); + $config = [ + 'Magento_Paypal/js/in-context/product-express-checkout' => [ + 'clientConfig' => $clientConfig + ] + ]; + + return $this->serializer->serialize($config); + } +} diff --git a/app/code/Magento/Paypal/Block/Express/Shortcut.php b/app/code/Magento/Paypal/Block/Express/Shortcut.php index bdb9279356d83..16305238e17de 100644 --- a/app/code/Magento/Paypal/Block/Express/Shortcut.php +++ b/app/code/Magento/Paypal/Block/Express/Shortcut.php @@ -137,7 +137,7 @@ public function __construct( } /** - * @return \Magento\Framework\View\Element\AbstractBlock + * @inheritdoc */ protected function _beforeToHtml() { @@ -145,7 +145,9 @@ protected function _beforeToHtml() $isInCatalog = $this->getIsInCatalogProduct(); - if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog)) { + if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog) + || (bool)(int)$this->config->getValue('in_context') + ) { $this->_shouldRender = false; return $result; } @@ -186,6 +188,8 @@ protected function _toHtml() } /** + * Check if we should render component + * * @return bool */ protected function shouldRender() diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php index fa131f9591fa9..7ad8fe658ec16 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Action\Action as AppAction; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Quote\Api\Data\CartInterface; /** * Abstract Express Checkout Controller @@ -132,12 +133,14 @@ public function __construct( /** * Instantiate quote and checkout * + * @param CartInterface|null $quoteObject + * * @return void * @throws \Magento\Framework\Exception\LocalizedException */ - protected function _initCheckout() + protected function _initCheckout(CartInterface $quoteObject = null) { - $quote = $this->_getQuote(); + $quote = $quoteObject ? $quoteObject : $this->_getQuote(); if (!$quote->hasItems() || $quote->getHasError()) { $this->getResponse()->setStatusHeader(403, '1.1', 'Forbidden'); throw new \Magento\Framework\Exception\LocalizedException(__('We can\'t initialize Express Checkout.')); diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php index cb1b3388dc06a..fc3a45e1e1397 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php @@ -6,7 +6,17 @@ */ namespace Magento\Paypal\Controller\Express\AbstractExpress; -class ShippingOptionsCallback extends \Magento\Paypal\Controller\Express\AbstractExpress +use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Paypal\Controller\Express\AbstractExpress; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\RequestInterface; + +/** + * Returns shipping rates by server-to-server request from PayPal. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShippingOptionsCallback extends AbstractExpress implements CsrfAwareActionInterface { /** * @var \Magento\Quote\Api\CartRepositoryInterface @@ -65,4 +75,21 @@ public function execute() $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } } + + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } } diff --git a/app/code/Magento/Paypal/Controller/Express/GetTokenData.php b/app/code/Magento/Paypal/Controller/Express/GetTokenData.php new file mode 100644 index 0000000000000..512dac4cdec06 --- /dev/null +++ b/app/code/Magento/Paypal/Controller/Express/GetTokenData.php @@ -0,0 +1,206 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Controller\Express; + +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Paypal\Model\Express\Checkout; +use Magento\Paypal\Model\Config; +use Magento\Framework\App\Action\Context; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Sales\Model\OrderFactory; +use Magento\Paypal\Model\Express\Checkout\Factory as CheckoutFactory; +use Magento\Framework\Session\Generic as PayPalSession; +use Magento\Framework\Url\Helper\Data as UrlHelper; +use Magento\Customer\Model\Url as CustomerUrl; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\GuestCartRepositoryInterface; +use Psr\Log\LoggerInterface; + +/** + * Retrieve paypal token + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GetTokenData extends AbstractExpress implements HttpGetActionInterface +{ + /** + * Config mode type + * + * @var string + */ + protected $_configType = Config::class; + + /** + * Config method type + * + * @var string + */ + protected $_configMethod = Config::METHOD_WPP_EXPRESS; + + /** + * Checkout mode type + * + * @var string + */ + protected $_checkoutType = Checkout::class; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @param Context $context + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + * @param OrderFactory $orderFactory + * @param CheckoutFactory $checkoutFactory + * @param PayPalSession $paypalSession + * @param UrlHelper $urlHelper + * @param CustomerUrl $customerUrl + * @param LoggerInterface $logger + * @param CustomerRepository $customerRepository + * @param CartRepositoryInterface $cartRepository + * @param GuestCartRepositoryInterface $guestCartRepository + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + Context $context, + CustomerSession $customerSession, + CheckoutSession $checkoutSession, + OrderFactory $orderFactory, + CheckoutFactory $checkoutFactory, + PayPalSession $paypalSession, + UrlHelper $urlHelper, + CustomerUrl $customerUrl, + LoggerInterface $logger, + CustomerRepository $customerRepository, + CartRepositoryInterface $cartRepository, + GuestCartRepositoryInterface $guestCartRepository + ) { + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $orderFactory, + $checkoutFactory, + $paypalSession, + $urlHelper, + $customerUrl + ); + + $this->logger = $logger; + $this->customerRepository = $customerRepository; + $this->cartRepository = $cartRepository; + $this->guestCartRepository = $guestCartRepository; + } + + /** + * Get token data + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $controllerResult = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $responseContent = [ + 'success' => true, + 'error_message' => '', + ]; + + try { + $token = $this->getToken(); + if ($token === null) { + $token = false; + } + $this->_initToken($token); + + $responseContent['token'] = $token; + } catch (LocalizedException $exception) { + $this->logger->critical($exception); + + $responseContent['success'] = false; + $responseContent['error_message'] = $exception->getMessage(); + } catch (\Exception $exception) { + $this->logger->critical($exception); + + $responseContent['success'] = false; + $responseContent['error_message'] = __('Sorry, but something went wrong'); + } + + return $controllerResult->setData($responseContent); + } + + /** + * Get paypal token + * + * @return string|null + * @throws LocalizedException + */ + private function getToken(): ?string + { + $quoteId = $this->getRequest()->getParam('quote_id'); + $customerId = $this->getRequest()->getParam('customer_id') ?: $this->_customerSession->getId(); + $hasButton = (bool)$this->getRequest()->getParam(Checkout::PAYMENT_INFO_BUTTON); + + if ($quoteId) { + $quote = $customerId ? $this->cartRepository->get($quoteId) : $this->guestCartRepository->get($quoteId); + } else { + $quote = $this->_getQuote(); + } + + $this->_initCheckout($quote); + + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $quote->removeAllAddresses(); + } + + if ($customerId) { + $customerData = $this->customerRepository->getById((int)$customerId); + + $this->_checkout->setCustomerWithAddressChange( + $customerData, + $quote->getBillingAddress(), + $quote->getShippingAddress() + ); + } + + // giropay urls + $this->_checkout->prepareGiropayUrls( + $this->_url->getUrl('checkout/onepage/success'), + $this->_url->getUrl('paypal/express/cancel'), + $this->_url->getUrl('checkout/onepage/success') + ); + + return $this->_checkout->start( + $this->_url->getUrl('*/*/return'), + $this->_url->getUrl('*/*/cancel'), + $hasButton + ); + } +} diff --git a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php new file mode 100644 index 0000000000000..62f4c4c4c457a --- /dev/null +++ b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php @@ -0,0 +1,172 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Controller\Express; + +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Paypal\Model\Config as PayPalConfig; +use Magento\Paypal\Model\Express\Checkout as PayPalCheckout; +use Magento\Paypal\Model\Api\ProcessableException as ApiProcessableException; +use Magento\Framework\App\Action\Context; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Sales\Model\OrderFactory; +use Magento\Paypal\Model\Express\Checkout\Factory as CheckoutFactory; +use Magento\Framework\Session\Generic as PayPalSession; +use Magento\Framework\Url\Helper\Data as UrlHelper; +use Magento\Customer\Model\Url as CustomerUrl; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Framework\UrlInterface; +use Magento\Quote\Api\GuestCartRepositoryInterface; + +/** + * Processes data after returning from PayPal + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class OnAuthorization extends AbstractExpress implements HttpPostActionInterface +{ + /** + * @inheritdoc + */ + protected $_configType = PayPalConfig::class; + + /** + * @inheritdoc + */ + protected $_configMethod = PayPalConfig::METHOD_WPP_EXPRESS; + + /** + * @inheritdoc + */ + protected $_checkoutType = PayPalCheckout::class; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * Url Builder + * + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @var GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @param Context $context + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + * @param OrderFactory $orderFactory + * @param CheckoutFactory $checkoutFactory + * @param PayPalSession $paypalSession + * @param UrlHelper $urlHelper + * @param CustomerUrl $customerUrl + * @param CartRepositoryInterface $cartRepository + * @param UrlInterface $urlBuilder + * @param GuestCartRepositoryInterface $guestCartRepository + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + Context $context, + CustomerSession $customerSession, + CheckoutSession $checkoutSession, + OrderFactory $orderFactory, + CheckoutFactory $checkoutFactory, + PayPalSession $paypalSession, + UrlHelper $urlHelper, + CustomerUrl $customerUrl, + CartRepositoryInterface $cartRepository, + UrlInterface $urlBuilder, + GuestCartRepositoryInterface $guestCartRepository + ) { + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $orderFactory, + $checkoutFactory, + $paypalSession, + $urlHelper, + $customerUrl + ); + $this->cartRepository = $cartRepository; + $this->urlBuilder = $urlBuilder; + $this->guestCartRepository = $guestCartRepository; + } + + /** + * Place order or redirect on Paypal review page + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $controllerResult = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $quoteId = $this->getRequest()->getParam('quoteId'); + $payerId = $this->getRequest()->getParam('payerId'); + $tokenId = $this->getRequest()->getParam('paymentToken'); + $customerId = $this->getRequest()->getParam('customerId') ?: $this->_customerSession->getId(); + + try { + if ($quoteId) { + $quote = $customerId ? $this->cartRepository->get($quoteId) : $this->guestCartRepository->get($quoteId); + } else { + $quote = $this->_getQuote(); + } + + $responseContent = [ + 'success' => true, + 'error_message' => '', + ]; + + /** Populate checkout object with new data */ + $this->_initCheckout($quote); + /** Populate quote with information about billing and shipping addresses*/ + $this->_checkout->returnFromPaypal($tokenId, $payerId); + if ($this->_checkout->canSkipOrderReviewStep()) { + $this->_checkout->place($tokenId); + $order = $this->_checkout->getOrder(); + /** "last successful quote" */ + $this->_getCheckoutSession()->setLastQuoteId($quote->getId())->setLastSuccessQuoteId($quote->getId()); + + $this->_getCheckoutSession()->setLastOrderId($order->getId()) + ->setLastRealOrderId($order->getIncrementId()) + ->setLastOrderStatus($order->getStatus()); + + $this->_eventManager->dispatch( + 'paypal_express_place_order_success', + [ + 'order' => $order, + 'quote' => $quote + ] + ); + $responseContent['redirectUrl'] = $this->urlBuilder->getUrl('checkout/onepage/success/'); + } else { + $responseContent['redirectUrl'] = $this->urlBuilder->getUrl('paypal/express/review'); + $this->_checkoutSession->setQuoteId($quote->getId()); + } + } catch (ApiProcessableException $e) { + $responseContent['success'] = false; + $responseContent['error_message'] = $e->getUserMessage(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $responseContent['success'] = false; + $responseContent['error_message'] = $e->getMessage(); + } catch (\Exception $e) { + $responseContent['success'] = false; + $responseContent['error_message'] = __('We can\'t process Express Checkout approval.'); + } + + return $controllerResult->setData($responseContent); + } +} diff --git a/app/code/Magento/Paypal/Controller/Ipn/Index.php b/app/code/Magento/Paypal/Controller/Ipn/Index.php index 4bcc3a9b3606c..a879266bc1915 100644 --- a/app/code/Magento/Paypal/Controller/Ipn/Index.php +++ b/app/code/Magento/Paypal/Controller/Ipn/Index.php @@ -4,6 +4,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Paypal\Controller\Ipn; @@ -16,6 +17,8 @@ /** * Unified IPN controller for all supported PayPal methods + * + * @SuppressWarnings(PHPMD.AllPurposeAction) */ class Index extends \Magento\Framework\App\Action\Action implements CsrfAwareActionInterface { @@ -73,7 +76,6 @@ public function validateForCsrf(RequestInterface $request): ?bool * Instantiate IPN model and pass IPN request to it * * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ public function execute() { @@ -95,6 +97,7 @@ public function execute() $this->_logger->critical($e); $this->getResponse()->setStatusHeader(503, '1.1', 'Service Unavailable')->sendResponse(); /** @todo eliminate usage of exit statement */ + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit; } catch (\Exception $e) { $this->_logger->critical($e); diff --git a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php index 85907c9d371ab..e2701bab1f062 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php +++ b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php @@ -12,6 +12,7 @@ use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Session\Generic; use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; use Magento\Quote\Model\Quote; @@ -40,7 +41,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action implements private $secureTokenService; /** - * @var SessionManager + * @var SessionManager|SessionManagerInterface */ private $sessionManager; @@ -56,6 +57,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action implements * @param SecureToken $secureTokenService * @param SessionManager $sessionManager * @param Transparent $transparent + * @param SessionManagerInterface|null $sessionInterface */ public function __construct( Context $context, @@ -63,12 +65,13 @@ public function __construct( Generic $sessionTransparent, SecureToken $secureTokenService, SessionManager $sessionManager, - Transparent $transparent + Transparent $transparent, + SessionManagerInterface $sessionInterface = null ) { $this->resultJsonFactory = $resultJsonFactory; $this->sessionTransparent = $sessionTransparent; $this->secureTokenService = $secureTokenService; - $this->sessionManager = $sessionManager; + $this->sessionManager = $sessionInterface ?: $sessionManager; $this->transparent = $transparent; parent::__construct($context); } @@ -83,7 +86,11 @@ public function execute() /** @var Quote $quote */ $quote = $this->sessionManager->getQuote(); - if (!$quote or !$quote instanceof Quote) { + if (!$quote || !$quote instanceof Quote) { + return $this->getErrorResponse(); + } + + if (!$this->transparent->isActive($quote->getStoreId())) { return $this->getErrorResponse(); } @@ -107,6 +114,8 @@ public function execute() } /** + * Get error response. + * * @return Json */ private function getErrorResponse() diff --git a/app/code/Magento/Paypal/CustomerData/BillingAgreement.php b/app/code/Magento/Paypal/CustomerData/BillingAgreement.php index 2c4cdf55bff92..a6304f32197bf 100644 --- a/app/code/Magento/Paypal/CustomerData/BillingAgreement.php +++ b/app/code/Magento/Paypal/CustomerData/BillingAgreement.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -93,7 +93,7 @@ public function getSectionData() [\Magento\Paypal\Model\Express\Checkout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT => 1] ) ), - 'confirmMessage' => $this->escaper->escapeJs( + 'confirmMessage' => $this->escaper->escapeHtml( __('Would you like to sign a billing agreement to streamline further purchases with PayPal?') ) ]; diff --git a/app/code/Magento/Paypal/Model/AbstractConfig.php b/app/code/Magento/Paypal/Model/AbstractConfig.php index e5beddac3b189..41f122ed9b3c9 100644 --- a/app/code/Magento/Paypal/Model/AbstractConfig.php +++ b/app/code/Magento/Paypal/Model/AbstractConfig.php @@ -9,7 +9,6 @@ use Magento\Payment\Model\Method\ConfigInterface; use Magento\Payment\Model\MethodInterface; use Magento\Store\Model\ScopeInterface; -use Magento\Paypal\Model\Config; use Magento\Framework\App\ObjectManager; /** @@ -293,11 +292,15 @@ public function isMethodActive($method) break; case Config::METHOD_WPS_BML: case Config::METHOD_WPP_BML: - $isEnabled = $this->_scopeConfig->isSetFlag( - 'payment/' . Config::METHOD_WPS_BML .'/active', + $disabledFunding = $this->_scopeConfig->getValue( + 'payment/paypal_express/disable_funding_options', ScopeInterface::SCOPE_STORE, $this->_storeId - ) + ); + $isExpressCreditEnabled = $disabledFunding + ? strpos($disabledFunding, 'CREDIT') === false + : true; + $isEnabled = $isExpressCreditEnabled || $this->_scopeConfig->isSetFlag( 'payment/' . Config::METHOD_WPP_BML .'/active', ScopeInterface::SCOPE_STORE, diff --git a/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php b/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php index 2ebe088d31d86..8965684d1085f 100644 --- a/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php +++ b/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php @@ -6,7 +6,7 @@ namespace Magento\Paypal\Model\Billing; /** - * Billing Agreement abstaract class + * Billing Agreement abstract class */ abstract class AbstractAgreement extends \Magento\Framework\Model\AbstractModel { diff --git a/app/code/Magento/Paypal/Model/Config.php b/app/code/Magento/Paypal/Model/Config.php index b058ba129a33f..9891f68bf8741 100644 --- a/app/code/Magento/Paypal/Model/Config.php +++ b/app/code/Magento/Paypal/Model/Config.php @@ -1635,6 +1635,7 @@ protected function _mapWpukFieldset($fieldName) * * @param string $fieldName * @return string|null + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _mapGenericStyleFieldset($fieldName) { @@ -1645,9 +1646,10 @@ protected function _mapGenericStyleFieldset($fieldName) case 'paypal_hdrbackcolor': case 'paypal_hdrbordercolor': case 'paypal_payflowcolor': + case 'disable_funding_options': return "paypal/style/{$fieldName}"; default: - return null; + return $this->mapButtonStyles($fieldName); } } @@ -1697,6 +1699,36 @@ protected function _mapMethodFieldset($fieldName) } } + /** + * Map PayPal button style config fields + * + * @param string $fieldName + * @return null|string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function mapButtonStyles(string $fieldName) + { + $page = substr($fieldName, 0, (int)strpos($fieldName, '_page_button_')); + + if (!$page) { + return null; + } + + switch ($fieldName) { + case "{$page}_page_button_customize": + case "{$page}_page_button_layout": + case "{$page}_page_button_size": + case "{$page}_page_button_color": + case "{$page}_page_button_shape": + case "{$page}_page_button_label": + case "{$page}_page_button_mx_installment_period": + case "{$page}_page_button_br_installment_period": + return "paypal/style/{$fieldName}"; + default: + return null; + } + } + /** * Payment API authentication methods source getter * diff --git a/app/code/Magento/Paypal/Model/Config/Rules/Converter.php b/app/code/Magento/Paypal/Model/Config/Rules/Converter.php index 2bae810a5fde1..2baedaa38f5a5 100644 --- a/app/code/Magento/Paypal/Model/Config/Rules/Converter.php +++ b/app/code/Magento/Paypal/Model/Config/Rules/Converter.php @@ -63,6 +63,7 @@ protected function createEvents(\DOMElement $node) if ($this->hasNodeElement($child)) { $result[$child->getAttribute('name')] = [ 'value' => $child->getAttribute('value'), + 'include' => $child->getAttribute('include'), 'predicate' => $this->createPredicate($child), ]; } diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php index 33fe5ec33002b..e52a85da3e829 100644 --- a/app/code/Magento/Paypal/Model/Express.php +++ b/app/code/Magento/Paypal/Model/Express.php @@ -44,7 +44,7 @@ class Express extends \Magento\Payment\Model\Method\AbstractMethod * * @var bool */ - protected $_isGateway = false; + protected $_isGateway = true; /** * Availability option diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 856e01f7353f2..72f166e8d07c1 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -21,6 +21,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Checkout { @@ -606,10 +607,12 @@ public function canSkipOrderReviewStep() * export shipping address in case address absence * * @param string $token + * @param string|null $payerIdentifier * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function returnFromPaypal($token) + public function returnFromPaypal($token, string $payerIdentifier = null) { $this->_getApi() ->setToken($token) @@ -685,7 +688,8 @@ public function returnFromPaypal($token) $payment = $quote->getPayment(); $payment->setMethod($this->_methodType); $this->_paypalInfo->importToPayment($this->_getApi(), $payment); - $payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $this->_getApi()->getPayerId()) + $payerId = $payerIdentifier ? : $this->_getApi()->getPayerId(); + $payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $payerId) ->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_TOKEN, $token); $quote->collectTotals(); $this->quoteRepository->save($quote); @@ -1072,6 +1076,7 @@ protected static function cmpShippingOptions(DataObject $option1, DataObject $op */ protected function _matchShippingMethodCode(Address $address, $selectedCode) { + $address->collectShippingRates(); $options = $this->_prepareShippingOptions($address, false); foreach ($options as $option) { if ($selectedCode === $option['code'] // the proper case as outlined in documentation diff --git a/app/code/Magento/Paypal/Model/ExpressConfigProvider.php b/app/code/Magento/Paypal/Model/ExpressConfigProvider.php index 518e8b12bfcd5..c8adb137299fa 100644 --- a/app/code/Magento/Paypal/Model/ExpressConfigProvider.php +++ b/app/code/Magento/Paypal/Model/ExpressConfigProvider.php @@ -66,14 +66,20 @@ class ExpressConfigProvider implements ConfigProviderInterface protected $urlBuilder; /** - * Constructor - * + * @var SmartButtonConfig + */ + private $smartButtonConfig; + + /** + * ExpressConfigProvider constructor. * @param ConfigFactory $configFactory * @param ResolverInterface $localeResolver * @param CurrentCustomer $currentCustomer * @param PaypalHelper $paypalHelper * @param PaymentHelper $paymentHelper * @param UrlInterface $urlBuilder + * @param SmartButtonConfig|null $smartButtonConfig + * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( ConfigFactory $configFactory, @@ -81,7 +87,8 @@ public function __construct( CurrentCustomer $currentCustomer, PaypalHelper $paypalHelper, PaymentHelper $paymentHelper, - UrlInterface $urlBuilder + UrlInterface $urlBuilder, + SmartButtonConfig $smartButtonConfig ) { $this->localeResolver = $localeResolver; $this->config = $configFactory->create(); @@ -89,6 +96,7 @@ public function __construct( $this->paypalHelper = $paypalHelper; $this->paymentHelper = $paymentHelper; $this->urlBuilder = $urlBuilder; + $this->smartButtonConfig = $smartButtonConfig; foreach ($this->methodCodes as $code) { $this->methods[$code] = $this->paymentHelper->getMethodInstance($code); @@ -96,7 +104,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { @@ -123,15 +131,17 @@ public function getConfig() $config['payment']['paypalExpress']['inContextConfig'] = [ 'inContextId' => self::IN_CONTEXT_BUTTON_ID, 'merchantId' => $this->config->getValue('merchant_id'), - 'path' => $this->urlBuilder->getUrl('paypal/express/gettoken', ['_secure' => true]), - 'clientConfig' => [ - 'environment' => ((int) $this->config->getValue('sandbox_flag') ? 'sandbox' : 'production'), - 'locale' => $locale, - 'button' => [ - self::IN_CONTEXT_BUTTON_ID - ] + ]; + $clientConfig = [ + 'button' => [ + self::IN_CONTEXT_BUTTON_ID ], + 'getTokenUrl' => $this->urlBuilder->getUrl('paypal/express/getTokenData'), + 'onAuthorizeUrl' => $this->urlBuilder->getUrl('paypal/express/onAuthorization'), + 'onCancelUrl' => $this->urlBuilder->getUrl('paypal/express/cancel') ]; + $clientConfig = array_replace_recursive($clientConfig, $this->smartButtonConfig->getConfig('checkout')); + $config['payment']['paypalExpress']['inContextConfig']['clientConfig'] = $clientConfig; } foreach ($this->methodCodes as $code) { @@ -146,6 +156,8 @@ public function getConfig() } /** + * Return setting value for in context checkout + * * @return bool */ protected function isInContextCheckout() diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php index 06a8a5b680bf4..7143576b71a07 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Paypal\Model\Payflow\Service\Response; use Magento\Framework\DataObject; +use Magento\Framework\Intl\DateTimeFactory; use Magento\Payment\Model\Method\Logger; use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; use Magento\Framework\Session\Generic; @@ -18,6 +21,7 @@ /** * Class Transaction + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Transaction { @@ -51,6 +55,11 @@ class Transaction */ private $logger; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param Generic $sessionTransparent * @param CartRepositoryInterface $quoteRepository @@ -58,6 +67,7 @@ class Transaction * @param PaymentMethodManagementInterface $paymentManagement * @param HandlerInterface $errorHandler * @param Logger $logger + * @param DateTimeFactory $dateTimeFactory */ public function __construct( Generic $sessionTransparent, @@ -65,7 +75,8 @@ public function __construct( Transparent $transparent, PaymentMethodManagementInterface $paymentManagement, HandlerInterface $errorHandler, - Logger $logger + Logger $logger, + DateTimeFactory $dateTimeFactory ) { $this->sessionTransparent = $sessionTransparent; $this->quoteRepository = $quoteRepository; @@ -73,6 +84,7 @@ public function __construct( $this->paymentManagement = $paymentManagement; $this->errorHandler = $errorHandler; $this->logger = $logger; + $this->dateTimeFactory = $dateTimeFactory; } /** @@ -114,8 +126,45 @@ public function savePaymentInQuote($response) $payment->setData(OrderPaymentInterface::CC_TYPE, $response->getData(OrderPaymentInterface::CC_TYPE)); $payment->setAdditionalInformation(Payflowpro::PNREF, $response->getData(Payflowpro::PNREF)); + $expDate = $response->getData('expdate'); + $expMonth = $this->getCcExpMonth($expDate); + $payment->setCcExpMonth($expMonth); + $expYear = $this->getCcExpYear($expDate); + $payment->setCcExpYear($expYear); + $this->errorHandler->handle($payment, $response); $this->paymentManagement->set($quote->getId(), $payment); } + + /** + * Extracts expiration month from PayPal response expiration date. + * + * @param string $expDate format {MMYY} + * @return int + */ + private function getCcExpMonth(string $expDate): int + { + return (int)substr($expDate, 0, 2); + } + + /** + * Extracts expiration year from PayPal response expiration date. + * + * @param string $expDate format {MMYY} + * @return int + */ + private function getCcExpYear(string $expDate): int + { + $last2YearDigits = (int)substr($expDate, 2, 2); + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $first2YearDigits = (int)substr($currentDate->format('Y'), 0, 2); + + // case when credit card expires at next century + if ((int)$currentDate->format('y') > $last2YearDigits) { + $first2YearDigits++; + } + + return 100 * $first2YearDigits + $last2YearDigits; + } } diff --git a/app/code/Magento/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php index aeea4c75933b5..2ba72c4b26bd7 100644 --- a/app/code/Magento/Paypal/Model/Payflowpro.php +++ b/app/code/Magento/Paypal/Model/Payflowpro.php @@ -894,7 +894,7 @@ public function addRequestOrderInfo(DataObject $request, Order $order) $orderIncrementId = $order->getIncrementId(); $request->setCustref($orderIncrementId) ->setInvnum($orderIncrementId) - ->setComment1($orderIncrementId); + ->setData('comment1', $orderIncrementId); } /** diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php new file mode 100644 index 0000000000000..80a0d477216b0 --- /dev/null +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Model; + +use Magento\Framework\Locale\ResolverInterface; + +/** + * Smart button config + */ +class SmartButtonConfig +{ + /** + * @var \Magento\Framework\Locale\ResolverInterface + */ + private $localeResolver; + + /** + * @var ConfigFactory + */ + private $config; + + /** + * @var array + */ + private $defaultStyles; + + /** + * @var array + */ + private $allowedFunding; + + /** + * @param ResolverInterface $localeResolver + * @param ConfigFactory $configFactory + * @param array $defaultStyles + * @param array $allowedFunding + */ + public function __construct( + ResolverInterface $localeResolver, + ConfigFactory $configFactory, + $defaultStyles = [], + $allowedFunding = [] + ) { + $this->localeResolver = $localeResolver; + $this->config = $configFactory->create(); + $this->config->setMethod(Config::METHOD_EXPRESS); + $this->defaultStyles = $defaultStyles; + $this->allowedFunding = $allowedFunding; + } + + /** + * Get smart button config + * + * @param string $page + * @return array + */ + public function getConfig(string $page): array + { + return [ + 'merchantId' => $this->config->getValue('merchant_id'), + 'environment' => ((int)$this->config->getValue('sandbox_flag') ? 'sandbox' : 'production'), + 'locale' => $this->localeResolver->getLocale(), + 'allowedFunding' => $this->getAllowedFunding($page), + 'disallowedFunding' => $this->getDisallowedFunding(), + 'styles' => $this->getButtonStyles($page) + ]; + } + + /** + * Returns disallowed funding from configuration + * + * @return array + */ + private function getDisallowedFunding(): array + { + $disallowedFunding = $this->config->getValue('disable_funding_options'); + return $disallowedFunding ? explode(',', $disallowedFunding) : []; + } + + /** + * Returns allowed funding + * + * @param string $page + * @return array + */ + private function getAllowedFunding(string $page): array + { + return array_values(array_diff($this->allowedFunding[$page], $this->getDisallowedFunding())); + } + + /** + * Returns button styles based on configuration + * + * @param string $page + * @return array + */ + private function getButtonStyles(string $page): array + { + $styles = $this->defaultStyles[$page]; + if ((boolean)$this->config->getValue("{$page}_page_button_customize")) { + $styles['layout'] = $this->config->getValue("{$page}_page_button_layout"); + $styles['size'] = $this->config->getValue("{$page}_page_button_size"); + $styles['color'] = $this->config->getValue("{$page}_page_button_color"); + $styles['shape'] = $this->config->getValue("{$page}_page_button_shape"); + $styles['label'] = $this->config->getValue("{$page}_page_button_label"); + + $styles = $this->updateStyles($styles, $page); + } + return $styles; + } + + /** + * Update styles based on locale and labels + * + * @param array $styles + * @param string $page + * @return array + */ + private function updateStyles(array $styles, string $page): array + { + $locale = $this->localeResolver->getLocale(); + + $installmentPeriodLocale = [ + 'en_MX' => 'mx', + 'es_MX' => 'mx', + 'en_BR' => 'br', + 'pt_BR' => 'br' + ]; + + // Credit label cannot be used with any custom color option or vertical layout. + if ($styles['label'] === 'credit') { + $styles['color'] = 'darkblue'; + $styles['layout'] = 'horizontal'; + } + + // Installment label is only available for specific locales + if ($styles['label'] === 'installment') { + if (array_key_exists($locale, $installmentPeriodLocale)) { + $styles['installmentperiod'] = (int)$this->config->getValue( + $page .'_page_button_' . $installmentPeriodLocale[$locale] . '_installment_period' + ); + } else { + $styles['label'] = 'paypal'; + } + } + + return $styles; + } +} diff --git a/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php b/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php new file mode 100644 index 0000000000000..8ad55d045ff1a --- /dev/null +++ b/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Model\System\Config\Source; + +/** + * Get button style options + */ +class ButtonStyles +{ + /** + * Button color source getter + * + * @return array + */ + public function getColor(): array + { + return [ + 'gold' => __('Gold'), + 'blue' => __('Blue'), + 'silver' => __('Silver'), + 'black' => __('Black') + ]; + } + + /** + * Button layout source getter + * + * @return array + */ + public function getLayout(): array + { + return [ + 'vertical' => __('Vertical'), + 'horizontal' => __('Horizontal') + ]; + } + + /** + * Button shape source getter + * + * @return array + */ + public function getShape(): array + { + return [ + 'pill' => __('Pill'), + 'rect' => __('Rectangle') + ]; + } + + /** + * Button size source getter + * + * @return array + */ + public function getSize(): array + { + return [ + 'medium' => __('Medium'), + 'large' => __('Large'), + 'responsive' => __('Responsive') + ]; + } + + /** + * Button label source getter + * + * @return array + */ + public function getLabel(): array + { + return [ + 'checkout' => __('Checkout'), + 'pay' => __('Pay'), + 'buynow' => __('Buy Now'), + 'paypal' => __('PayPal'), + 'installment' => __('Installment'), + 'credit' => __('Credit') + ]; + } + + /** + * Brazil button installment period source getter + * + * @return array + */ + public function getBrInstallmentPeriod(): array + { + $numbers = range(2, 12); + + return array_combine($numbers, $numbers); + } + + /** + * Mexico button installment period source getter + * + * @return array + */ + public function getMxInstallmentPeriod(): array + { + $numbers = range(3, 12, 3); + + return array_combine($numbers, $numbers); + } +} diff --git a/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php b/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php new file mode 100644 index 0000000000000..1a9cfe0998fb8 --- /dev/null +++ b/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Model\System\Config\Source; + +/** + * Get disable funding options + */ +class DisableFundingOptions +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => 'CREDIT', + 'label' => __('PayPal Credit') + ], + [ + 'value' => 'CARD', + 'label' => __('PayPal Guest Checkout Credit Card Icons') + ], + [ + 'value' => 'ELV', + 'label' => __('Elektronisches Lastschriftverfahren - German ELV') + ] + ]; + } +} diff --git a/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php b/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php index 58edf68f3475e..861ca74060680 100644 --- a/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php +++ b/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php @@ -9,6 +9,8 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Paypal\Model\Config as PaypalConfig; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Paypal\Block\Express\InContext\Minicart\SmartButton as MinicartSmartButton; +use Magento\Paypal\Block\Express\InContext\SmartButton as SmartButton; /** * PayPal module observer @@ -50,8 +52,9 @@ public function execute(EventObserver $observer) /** @var \Magento\Catalog\Block\ShortcutButtons $shortcutButtons */ $shortcutButtons = $observer->getEvent()->getContainer(); $blocks = [ - \Magento\Paypal\Block\Express\InContext\Minicart\Button::class => + MinicartSmartButton::class => PaypalConfig::METHOD_WPS_EXPRESS, + SmartButton::class => PaypalConfig::METHOD_WPS_EXPRESS, \Magento\Paypal\Block\Express\Shortcut::class => PaypalConfig::METHOD_WPP_EXPRESS, \Magento\Paypal\Block\Bml\Shortcut::class => PaypalConfig::METHOD_WPP_EXPRESS, \Magento\Paypal\Block\WpsExpress\Shortcut::class => PaypalConfig::METHOD_WPS_EXPRESS, @@ -77,11 +80,9 @@ public function execute(EventObserver $observer) '', $params ); - $shortcut->setIsInCatalogProduct( - $observer->getEvent()->getIsCatalogProduct() - )->setShowOrPosition( - $observer->getEvent()->getOrPosition() - ); + $shortcut->setIsInCatalogProduct($observer->getEvent()->getIsCatalogProduct()) + ->setShowOrPosition($observer->getEvent()->getOrPosition()) + ->setIsShoppingCart((bool) $observer->getEvent()->getIsShoppingCart()); $shortcutButtons->addShortcut($shortcut); } } diff --git a/app/code/Magento/Paypal/README.md b/app/code/Magento/Paypal/README.md index 8f4453ae0a058..0ed4f2e90291b 100644 --- a/app/code/Magento/Paypal/README.md +++ b/app/code/Magento/Paypal/README.md @@ -1,6 +1,6 @@ Module Magento\PayPal implements integration with the PayPal payment system. Namely, it enables the following payment methods: -*PayPal Express Checkout -*PayPal Payments Standard -*PayPal Payments Pro -*PayPal Credit -*PayFlow Payment Gateway +* PayPal Express Checkout +* PayPal Payments Standard +* PayPal Payments Pro +* PayPal Credit +* PayFlow Payment Gateway diff --git a/app/code/Magento/Paypal/Setup/Patch/Data/UpdatePaypalCreditOption.php b/app/code/Magento/Paypal/Setup/Patch/Data/UpdatePaypalCreditOption.php new file mode 100644 index 0000000000000..6c4362d83e29f --- /dev/null +++ b/app/code/Magento/Paypal/Setup/Patch/Data/UpdatePaypalCreditOption.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; + +/** + * Class AddPaypalOrderStates + */ +class UpdatePaypalCreditOption implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * PrepareInitialConfig constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $connection = $this->moduleDataSetup->getConnection(); + $select = $connection->select() + ->from($this->moduleDataSetup->getTable('core_config_data'), ['scope', 'scope_id', 'value']) + ->where('path = ?', 'payment/paypal_express_bml/active'); + foreach ($connection->fetchAll($select) as $pair) { + if (!$pair['value']) { + $this->moduleDataSetup->getConnection() + ->insertOnDuplicate( + $this->moduleDataSetup->getTable('core_config_data'), + [ + 'scope' => $pair['scope'], + 'scope_id' => $pair['scope_id'], + 'path' => 'paypal/style/disable_funding_options', + 'value' => 'CREDIT' + ] + ); + } + } + $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.3.1'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml new file mode 100644 index 0000000000000..32c2fab40e97a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.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="OpenPayPalButtonCheckoutPage"> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <click selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="openAdvancedSettingTab"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.frontendExperienceSettingsTab(countryCode)}}" stepKey="waitForFrontendExperienceSettingsTab"/> + <click selector="{{PayPalAdvancedSettingConfigSection.frontendExperienceSettingsTab(countryCode)}}" stepKey="openFrontendExperienceSettingsTab"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.checkoutPageTab(countryCode)}}" stepKey="waitForCheckoutPageTab"/> + <click selector="{{PayPalAdvancedSettingConfigSection.checkoutPageTab(countryCode)}}" stepKey="openCheckoutPageTab"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml new file mode 100644 index 0000000000000..08ca6c7834384 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml @@ -0,0 +1,40 @@ +<?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="EnablePayPalConfiguration"> + <arguments> + <argument name="payPalConfigType"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <waitForElementVisible selector="{{OtherPayPalPaymentsConfigSection.expandTab(countryCode)}}" stepKey="waitForOtherPayPalPaymentsSection"/> + <conditionalClick selector="{{OtherPayPalPaymentsConfigSection.expandTab(countryCode)}}" dependentSelector="{{OtherPayPalPaymentsConfigSection.expandedTab(countryCode)}}" visible="false" stepKey="clickOtherPayPalPaymentsSection"/> + <waitForElementVisible selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="waitForWPSExpressConfigureBtn"/> + <click selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="clickWPSExpressConfigureBtn"/> + <waitForElementVisible selector="{{payPalConfigType.enableSolution(countryCode)}}" stepKey="waitForWPSExpressEnable"/> + <selectOption selector="{{payPalConfigType.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableWPSExpressSolution"/> + <seeInPopup userInput="There is already another PayPal solution enabled. Enable this solution instead?" stepKey="seeAlertMessage"/> + <acceptPopup stepKey="acceptEnablePopUp"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + </actionGroup> + <actionGroup name="CheckEnableOptionPayPalConfiguration"> + <arguments> + <argument name="payPalConfigType"/> + <argument name="enabledOption" type="string"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <waitForElementVisible selector="{{OtherPayPalPaymentsConfigSection.expandTab(countryCode)}}" stepKey="waitForOtherPayPalPaymentsSection"/> + <conditionalClick selector="{{OtherPayPalPaymentsConfigSection.expandTab(countryCode)}}" dependentSelector="{{OtherPayPalPaymentsConfigSection.expandedTab(countryCode)}}" visible="false" stepKey="clickOtherPayPalPaymentsSection"/> + <waitForElementVisible selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="waitForWPSExpressConfigureBtn"/> + <click selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="clickWPSExpressConfigureBtn1"/> + <waitForElementVisible selector="{{payPalConfigType.enableSolution(countryCode)}}" stepKey="waitForWPSExpressEnable"/> + <seeOptionIsSelected selector="{{payPalConfigType.enableSolution(countryCode)}}" userInput="{{enabledOption}}" stepKey="seeSelectedOption"/> + <click selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="clickWPSExpressConfigureBtn2"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml new file mode 100644 index 0000000000000..bae517ffe2f3e --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml @@ -0,0 +1,73 @@ +<?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="ConfigPayPalExpressCheckout"> + <arguments> + <argument name="credentials" defaultValue="_CREDS"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.email(countryCode)}}" userInput="{{credentials.paypal_express_email}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod(countryCode)}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.username(countryCode)}}" userInput="{{credentials.paypal_express_api_username}}" stepKey="inputAPIUsername"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.password(countryCode)}}" userInput="{{credentials.paypal_express_api_password}}" stepKey="inputAPIPassword"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.paypal_express_api_signature}}" stepKey="inputAPISignature"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.paypal_express_merchantID}}" stepKey="inputMerchantID"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + </actionGroup> + <actionGroup name="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" extends="CreateOrderToPrintPageActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="clickPlaceOrder"/> + <!--set ID for iframe of PayPal group button--> + <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> + <!--switch to iframe of PayPal group button--> + <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> + <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> + <click selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="clickPayPalBtn"/> + <switchToIFrame stepKey="switchBack1"/> + <!--Check in-context--> + <comment userInput="Check in-context" stepKey="commentVerifyInContext"/> + <switchToNextTab stepKey="switchToInContentTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeCurrentUrlMatches regex="~\//www.sandbox.paypal.com/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> + <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{Payer.buyerEmail}}" stepKey="fillEmail"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{Payer.buyerPassword}}" stepKey="fillPassword"/> + <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> + <waitForPageLoad stepKey="wait"/> + <seeElement selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> + </actionGroup> + <actionGroup name="addProductToCheckoutPage"> + <arguments> + <argument name="Category"/> + </arguments> + <amOnPage url="{{StorefrontCategoryPage.url(Category.name)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="clickPayPalCheckbox"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..207bf62abf3ce --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,26 @@ +<?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="AdminMenuReportsSalesPayPalSettlement"> + <data key="pageTitle">PayPal Settlement Reports</data> + <data key="title">PayPal Settlement</data> + <data key="dataUiId">magento-paypal-report-salesroot-paypal-settlement-reports</data> + </entity> + <entity name="AdminMenuSales"> + <data key="pageTitle">Sales</data> + <data key="title">Sales</data> + <data key="dataUiId">magento-sales-sales</data> + </entity> + <entity name="AdminMenuSalesBillingAgreements"> + <data key="pageTitle">Billing Agreements</data> + <data key="title">Billing Agreements</data> + <data key="dataUiId">magento-paypal-paypal-billing-agreement</data> + </entity> +</entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index 6d5f80e30dc7f..ae34476e9ac0b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml @@ -38,6 +38,9 @@ <entity name="SampleUseProxy" type="use_proxy"> <data key="value">0</data> </entity> + <entity name="SampleMerchantID" type="use_proxy"> + <data key="value">someMerchantId</data> + </entity> <!-- default configuration used to restore Magento config --> <entity name="DefaultPayPalConfig" type="paypal_config_state"> @@ -58,4 +61,42 @@ <entity name="DefaultApiSignature" type="api_signature"> <data key="value"/> </entity> + <entity name="Payer" type="paypal_buyer"> + <data key="buyerEmail">buyer.mpi@gmail.com</data> + <data key="buyerPassword">12345678</data> + </entity> + <entity name="PayPalLabel" type="paypal"> + <data key="checkout">checkout</data> + <data key="credit">credit</data> + <data key="pay">pay</data> + <data key="buynow">buy now</data> + <data key="paypal">pay pal</data> + <data key="installment">installment</data> + </entity> + <entity name="PayPalLayout" type="paypal"> + <data key="horizontal">horizontal</data> + <data key="vertical">vertical</data> + </entity> + <entity name="PayPalSize" type="paypal"> + <data key="medium">medium</data> + <data key="large">large</data> + <data key="responsive">responsive</data> + </entity> + <entity name="PayPalShape" type="paypal"> + <data key="pill">pill</data> + <data key="rectangle">rectangle</data> + </entity> + <entity name="PayPalColor" type="paypal"> + <data key="gold">gold</data> + <data key="blue">blue</data> + <data key="silver">silver</data> + <data key="black">black</data> + </entity> + <entity name="SamplePaypalExpressConfig" type="paypal_express_config"> + <data key="paypal_express_email">myBusinessAccount@magento.com</data> + <data key="paypal_express_api_username">myApiUsername.magento.com</data> + <data key="paypal_express_api_password">somePassword</data> + <data key="paypal_express_api_signature">someApiSignature</data> + <data key="paypal_express_merchantID">someMerchantId</data> + </entity> </entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection.xml new file mode 100644 index 0000000000000..ca8438d5ee06a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection.xml @@ -0,0 +1,31 @@ +<?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="OtherPayPalPaymentsConfigSection"> + <element name="expandTab" type="button" selector="#payment_{{countryCode}}_other_paypal_payment_solutions-head" parameterized="true"/> + <element name="expandedTab" type="button" selector="#payment_{{countryCode}}_other_paypal_payment_solutions-head.open" parameterized="true"/> + </section> + <section name="WPSExpressConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_group_all_in_one_wps_express-head" parameterized="true"/> + <element name="enableSolution" type="input" selector="#payment_{{countryCode}}_paypal_group_all_in_one_wps_express_express_checkout_required_enable_express_checkout" parameterized="true"/> + </section> + <section name="PaymentsProHostedWithExpressCheckoutConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_group_all_in_one_payments_pro_hosted_solution_with_express_checkout-head" parameterized="true"/> + <element name="enableSolution" type="input" selector="#payment_{{countryCode}}_paypal_group_all_in_one_payments_pro_hosted_solution_with_express_checkout_pphs_required_settings_pphs_enable" parameterized="true"/> + </section> + <section name="WPSOtherConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_group_all_in_one_wps_other-head" parameterized="true"/> + <element name="enableSolution" type="input" selector="#payment_{{countryCode}}_paypal_group_all_in_one_wps_other_express_checkout_required_enable_express_checkout" parameterized="true"/> + </section> + <section name="WebsitePaymentsPlusConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_group_all_in_one_payments_pro_hosted_solution_{{countryCode}}-head" parameterized="true"/> + <element name="enableSolution" type="input" selector="#payment_{{countryCode}}_paypal_group_all_in_one_payments_pro_hosted_solution_{{countryCode}}_pphs_required_settings_pphs_enable" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml new file mode 100644 index 0000000000000..85f94cd8691a5 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml @@ -0,0 +1,62 @@ +<?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="PayPalExpressCheckoutConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}-head" parameterized="true"/> + <element name="email" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_express_checkout_required_express_checkout_business_account" parameterized="true"/> + <element name="apiMethod" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_express_checkout_required_express_checkout_api_authentication" parameterized="true"/> + <element name="username" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_express_checkout_required_express_checkout_api_username" parameterized="true"/> + <element name="password" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_express_checkout_required_express_checkout_api_password" parameterized="true"/> + <element name="signature" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_express_checkout_required_express_checkout_api_signature" parameterized="true"/> + <element name="sandboxMode" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_express_checkout_required_express_checkout_sandbox_flag" parameterized="true"/> + <element name="enableSolution" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_enable_express_checkout" parameterized="true"/> + <element name="merchantID" type="input" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_express_checkout_required_merchant_id" parameterized="true"/> + </section> + <section name="PayPalExpressCheckoutOtherCountryConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_express_checkout_other-head" parameterized="true"/> + <element name="enableSolution" type="input" selector="#payment_{{countryCode}}_express_checkout_other_express_checkout_required_enable_express_checkout" parameterized="true"/> + </section> + <section name="PayPalAdvancedSettingConfigSection"> + <element name="advancedSettingTab" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced-head" parameterized="true"/> + <element name="frontendExperienceSettingsTab" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_express_checkout_frontend-head" parameterized="true"/> + <element name="checkoutPageTab" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button-head" parameterized="true"/> + </section> + <section name="ButtonCustomization"> + <element name="customizeDrpDown" type="button" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button_checkout_page_button_customize']//select[contains(@data-ui-id, 'button-customize')]"/> + <element name="customizeNo" type="text" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button_checkout_page_button_customize']//select[contains(@data-ui-id, 'button-customize')]/option[@value='0' and @selected='selected']"/> + <element name="label" type="input" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button']//select[contains(@id, 'button_label')]"/> + <element name="layout" type="input" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button']//select[contains(@id, 'button_layout')]"/> + <element name="size" type="input" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button']//select[contains(@id, 'button_size')]"/> + <element name="shape" type="input" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button']//select[contains(@id, 'button_shape')]"/> + <element name="color" type="input" selector="//tr[@id='row_payment_us_paypal_alternative_payment_methods_express_checkout_us_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button']//select[contains(@id, 'button_color')]"/> + </section> + <section name="PayPalButtonOnStorefront"> + <element name="label" type="text" selector="[aria-label='{{label}}']" parameterized="true"/> + <element name="layout" type="text" selector="[data-layout='{{layout}}']" parameterized="true"/> + <element name="size" type="text" selector="[data-size='{{size}}']" parameterized="true"/> + <element name="shape" type="text" selector=".paypal-button-shape-{{shape}}" parameterized="true"/> + <element name="color" type="text" selector=".paypal-button-color-{{color}}" parameterized="true"/> + </section> + <section name="CheckoutPaymentSection"> + <element name="PayPalPaymentRadio" type="radio" selector="input#paypal_express.radio" timeout="30"/> + <element name="PayPalBtn" type="radio" selector=".paypal-button.paypal-button-number-0" timeout="30"/> + </section> + <section name="PayPalPaymentSection"> + <element name="guestCheckout" type="input" selector="#guest"/> + <element name="loginSection" type="input" selector=" #main>#login"/> + <element name="email" type="input" selector="//input[contains(@name, 'email') and not(contains(@style, 'display:none'))]"/> + <element name="password" type="input" selector="//input[contains(@name, 'password') and not(contains(@style, 'display:none'))]"/> + <element name="loginBtn" type="input" selector="button#btnLogin"/> + <element name="reviewUserInfo" type="text" selector="//p[@id='reviewUserInfo' and contains(text(),'Hi, MPI!')]"/> + <element name="cartIcon" type="text" selector="#transactionCart"/> + <element name="itemName" type="text" selector="//span[@title='{{productName}}']" parameterized="true"/> + <element name="PayPalSubmitBtn" type="text" selector="//input[@type='submit']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PaymentsConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PaymentsConfigSection.xml new file mode 100644 index 0000000000000..35162cb7d619d --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PaymentsConfigSection.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="PaymentsConfigSection"> + <element name="merchantCountry" type="select" selector="//select[@name='groups[account][fields][merchant_country][value]']"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml b/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml new file mode 100644 index 0000000000000..621f2e6a67688 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="PaypalTestSuite"> + <include> + <test name="CheckDefaultValueOfPayPalCustomizeButtonTest"/> + <test name="PayPalSmartButtonInCheckoutPage"/> + <test name="CheckCreditButtonConfiguration"/> + </include> + </suite> +</suites> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml new file mode 100644 index 0000000000000..b485fcb2a8f9a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml @@ -0,0 +1,276 @@ +<?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="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in United Kingdom"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country United Kingdom"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalExpressConfig"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set paypal/general/merchant_country US" stepKey="setMerchantCountry"/> + <magentoCLI command="config:set payment/paypal_express/active 0" stepKey="disablePayPalExpress"/> + <magentoCLI command="config:set payment/wps_express/active 0" stepKey="disableWPSExpress"/> + <magentoCLI command="config:set payment/hosted_pro/active 0" stepKey="disableHostedProExpress"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Change Merchant Country --> + <comment userInput="Change Merchant Country" stepKey="changeMerchantCountryComment"/> + <waitForElementVisible selector="{{PaymentsConfigSection.merchantCountry}}" stepKey="waitForMerchantCountry"/> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="United Kingdom" stepKey="setMerchantCountry"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!-- Enable WPS Express --> + <comment userInput="Enable WPS Express" stepKey="enableWPSExpressComment"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSExpressConfigSection"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <!-- Check only the correct solution is enabled --> + <comment userInput="Check only the correct solution is enabled" stepKey="checkOnlyTheCorrectSolutionIsEnabledComment1"/> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSExpressConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <!-- Enable Pro Hosted With Express Checkout --> + <comment userInput="Enable Pro Hosted With Express Checkout" stepKey="enableProHostedWithExpressCheckoutComment"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="PaymentsProHostedWithExpressCheckoutConfigSection"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <!-- Check only the correct solution is enabled --> + <comment userInput="Check only the correct solution is enabled" stepKey="checkOnlyTheCorrectSolutionIsEnabledComment2"/> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSExpressConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="PaymentsProHostedWithExpressCheckoutConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInJapan" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Japan"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Japan"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Japan" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInFrance" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in France"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country France"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="France" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInHongKong" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Hong Kong"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Hong Kong"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Hong Kong SAR China" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInItaly" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Italy"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Italy"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Italy" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="it"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInSpain" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Spain"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Spain"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Spain" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="es"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml index ac752e8412ff9..934449dfd136c 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml @@ -18,7 +18,7 @@ <testCaseId value="MAGETWO-92043"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml new file mode 100644 index 0000000000000..03f0167230e9f --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsPayPalSettlementNavigateMenuTest"> + <annotations> + <features value="Paypal"/> + <stories value="Menu Navigation"/> + <title value="Admin reports paypal settlement navigate menu test"/> + <description value="Admin should be able to navigate to Reports > PayPal Settlement"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14193"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToPayPalSettlementPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesPayPalSettlement.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesPayPalSettlement.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml new file mode 100644 index 0000000000000..8c3735fcbd253 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSalesBillingAgreementsNavigateMenuTest"> + <annotations> + <features value="Paypal"/> + <stories value="Menu Navigation"/> + <title value="Admin sales billing agreements navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Billing Agreements"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14194"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToBillingAgreementsPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesBillingAgreements.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesBillingAgreements.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml b/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml new file mode 100644 index 0000000000000..1858ee130a347 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml @@ -0,0 +1,170 @@ +<?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="CheckDefaultValueOfPayPalCustomizeButtonTest"> + <annotations> + <features value="PayPal"/> + <stories value="Button Configuration"/> + <title value="Check Default Value Of PayPal Customize Button"/> + <description value="Default value of PayPal Customize Button should be NO"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10904"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> + <seeElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <seeOptionIsSelected selector="{{ButtonCustomization.customizeDrpDown}}" userInput="No" stepKey="seeNoIsDefaultValue"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify default value--> + <comment userInput="Verify default value" stepKey="commentVerifyDefaultValue1"/> + <seeElement selector="{{ButtonCustomization.label}}" stepKey="seeLabel"/> + <seeElement selector="{{ButtonCustomization.layout}}" stepKey="seeLayout"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize1"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape1"/> + <seeElement selector="{{ButtonCustomization.color}}" stepKey="seeColor"/> + </test> + <test name="CheckCreditButtonConfiguration"> + <annotations> + <features value="PayPal"/> + <stories value="Button Configuration"/> + <title value="Check Credit Button Configuration"/> + <description value="Admin is able to customize Credit button"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10900"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Navigate to button configuration setting--> + <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> + <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify Credit Button value--> + <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> + <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize2"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape2"/> + <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> + <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> + <!--Customize Credit Button--> + <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> + <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSave"/> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="/" stepKey="openStorefront"/> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addProductToCheckoutPage" stepKey="addProductToCheckoutPage"> + <argument name="Category" value="$$createPreReqCategory$$"/> + </actionGroup> + <!--set ID for iframe of PayPal group button--> + <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> + <!--switch to iframe of PayPal group button--> + <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> + <switchToIframe userInput="myIframe" stepKey="clickPrintOrderLink"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> + </test> + <test name="PayPalSmartButtonInCheckoutPage"> + <annotations> + <features value="PayPal"/> + <stories value="Generic checkout skeleton flow"/> + <title value="Mainflow of PayPal Smart Button"/> + <description value="Users are able to place order using PayPal Smart Button"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13690"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + <magentoCLI command="config:set payment/paypal_express/in_context 1" stepKey="disableInContextPayPal"/> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + <magentoCLI command="config:set payment/paypal_express/in_context 0" stepKey="enableInContextPayPal"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <magentoCLI command="config:set payment/paypal_express/payment_action Authorization" stepKey="inputPaymentAction"/> + <magentoCLI command="config:set payment/paypal_express/solution_type Sole" stepKey="enablePayPalGuestCheckout"/> + <magentoCLI command="config:set payment/paypal_express/line_items_enabled 1" stepKey="enableTransferCartLine"/> + <magentoCLI command="config:set payment/paypal_express/skip_order_review_step 1" stepKey="enableSkipOrderReview"/> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Place an order using PayPal method--> + <comment userInput="Place an order using PayPal method" stepKey="commentPayPalPlaceOrder"/> + <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> + <argument name="Category" value="$$createPreReqCategory$$"/> + </actionGroup> + <!--Open Cart on PayPal--> + <comment userInput="Open Cart on PayPal" stepKey="commentOpenCart"/> + <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> + <seeElement selector="{{PayPalPaymentSection.itemName($$createPreReqProduct.name$$)}}" stepKey="seeProductname"/> + <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--I see order successful Page instead of Order Review Page--> + <comment userInput="I see order successful Page instead of Order Review Page" stepKey="commentVerifyOrderReviewPage"/> + <waitForElement selector="{{CheckoutSuccessMainSection.successTitle}}" stepKey="waitForLoadSuccessPageTitle"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="seeOrderLink"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php index b33d2f5723961..b9ea53c154014 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + /** * Class AbstractEnableTest * @@ -43,8 +45,18 @@ protected function setUp() )->disableOriginalConstructor() ->getMockForAbstractClass(); + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->elementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->elementMock, $escaper); + $this->abstractEnable = $objectManager->getObject( - \Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub::class + \Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub::class, + [ + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) + ] ); } diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php new file mode 100644 index 0000000000000..2c9a33ce43854 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Multiselect; + +use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use \Magento\Framework\Data\Form\Element\AbstractElement; +use \Magento\Framework\App\RequestInterface; +use \Magento\Framework\View\Helper\Js; +use \Magento\Paypal\Model\Config; +use \Magento\Paypal\Block\Adminhtml\System\Config\MultiSelect\DisabledFundingOptions; +use \Magento\Paypal\Model\Config\StructurePlugin; +use \PHPUnit\Framework\TestCase; + +/** + * Class DisabledFundingOptionsTest + */ +class DisabledFundingOptionsTest extends TestCase +{ + /** + * @var \Magento\Paypal\Block\Adminhtml\System\Config\Multiselect\DisabledFundingOptions + */ + private $model; + + /** + * @var \Magento\Framework\Data\Form\Element\AbstractElement + */ + private $element; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var \Magento\Framework\View\Helper\Js|\PHPUnit_Framework_MockObject_MockObject + */ + private $jsHelper; + + /** + * @var \Magento\Paypal\Model\Config + */ + private $config; + + protected function setUp() + { + $helper = new ObjectManager($this); + $this->element = $this->getMockForAbstractClass( + AbstractElement::class, + [], + '', + false, + true, + true, + ['getHtmlId', 'getElementHtml', 'getName'] + ); + $this->request = $this->getMockForAbstractClass(RequestInterface::class); + $this->jsHelper = $this->createMock(Js::class); + $this->config = $this->createMock(Config::class); + $this->element->setValues($this->getDefaultFundingOptions()); + $this->model = $helper->getObject( + DisabledFundingOptions::class, + ['request' => $this->request, 'jsHelper' => $this->jsHelper, 'config' => $this->config] + ); + } + + /** + * @param null|string $requestCountry + * @param null|string $merchantCountry + * @param bool $shouldContainPaypalCredit + * @dataProvider isPaypalCreditAvailableDataProvider + */ + public function testIsPaypalCreditAvailable( + ?string $requestCountry, + ?string $merchantCountry, + bool $shouldContainPaypalCredit + ) { + $this->request->expects($this->any()) + ->method('getParam') + ->will($this->returnCallback(function ($param) use ($requestCountry) { + if ($param == StructurePlugin::REQUEST_PARAM_COUNTRY) { + return $requestCountry; + } + return $param; + })); + $this->config->expects($this->any()) + ->method('getMerchantCountry') + ->will($this->returnCallback(function () use ($merchantCountry) { + return $merchantCountry; + })); + $this->model->render($this->element); + $payPalCreditOption = [ + 'value' => 'CREDIT', + 'label' => __('PayPal Credit') + ]; + $elementValues = $this->element->getValues(); + if ($shouldContainPaypalCredit) { + $this->assertContains($payPalCreditOption, $elementValues); + } else { + $this->assertNotContains($payPalCreditOption, $elementValues); + } + } + + /** + * @return array + */ + public function isPaypalCreditAvailableDataProvider(): array + { + return [ + [null, 'US', true], + ['US', 'US', true], + ['US', 'GB', true], + ['GB', 'GB', false], + ['GB', 'US', false], + ['GB', null, false], + ]; + } + + /** + * @inheritdoc + */ + private function getDefaultFundingOptions(): array + { + return [ + [ + 'value' => 'CREDIT', + 'label' => __('PayPal Credit') + ], + [ + 'value' => 'CARD', + 'label' => __('PayPal Guest Checkout Credit Card Icons') + ], + [ + 'value' => 'ELV', + 'label' => __('Elektronisches Lastschriftverfahren - German ELV') + ] + ]; + } +} diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php index b8558cdc08491..3fce5dab9dda7 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php @@ -8,6 +8,8 @@ use Magento\Catalog\Block as CatalogBlock; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Paypal\Model\ConfigFactory; +use Magento\Paypal\Model\Config; class ShortcutTest extends \PHPUnit\Framework\TestCase { @@ -33,12 +35,24 @@ protected function setUp() $this->paypalShortcutHelperMock = $this->createMock(\Magento\Paypal\Helper\Shortcut\ValidatorInterface::class); $this->objectManagerHelper = new ObjectManagerHelper($this); + $configFactoryMock = $this->getMockBuilder(ConfigFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->setMethods(['setMethod']) + ->getMock(); + $configFactoryMock->expects($this->any())->method('create')->willReturn($configMock); + $this->shortcut = $this->objectManagerHelper->getObject( \Magento\Paypal\Block\Bml\Shortcut::class, [ 'paymentData' => $this->paymentHelperMock, 'mathRandom' => $this->randomMock, 'shortcutValidator' => $this->paypalShortcutHelperMock, + 'config' => $configFactoryMock ] ); } diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php index 60451a9827097..6752eab6a7783 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php @@ -3,16 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Paypal\Test\Unit\Controller\Transparent; use Magento\Framework\App\Action\Context; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Session\Generic; use Magento\Framework\Session\SessionManager; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Paypal\Controller\Transparent\RequestSecureToken; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; +use PHPUnit\Framework\MockObject\MockObject; /** * Class RequestSecureTokenTest @@ -22,39 +24,39 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase { /** - * @var Transparent|\PHPUnit_Framework_MockObject_MockObject + * @var Transparent|MockObject */ - protected $transparentMock; + private $transparent; /** - * @var RequestSecureToken|\PHPUnit_Framework_MockObject_MockObject + * @var RequestSecureToken|MockObject */ - protected $controller; + private $controller; /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|MockObject */ - protected $contextMock; + private $context; /** - * @var JsonFactory|\PHPUnit_Framework_MockObject_MockObject + * @var JsonFactory|MockObject */ - protected $resultJsonFactoryMock; + private $resultJsonFactory; /** - * @var Generic|\PHPUnit_Framework_MockObject_MockObject + * @var Generic|MockObject */ - protected $sessionTransparentMock; + private $sessionTransparent; /** - * @var SecureToken|\PHPUnit_Framework_MockObject_MockObject + * @var SecureToken|MockObject */ - protected $secureTokenServiceMock; + private $secureTokenService; /** - * @var SessionManager|\PHPUnit_Framework_MockObject_MockObject + * @var SessionManager|MockObject */ - protected $sessionManagerMock; + private $sessionManager; /** * Set up @@ -64,45 +66,46 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) + $this->context = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) ->disableOriginalConstructor() ->getMock(); - $this->resultJsonFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) + $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->sessionTransparentMock = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) + $this->sessionTransparent = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) ->setMethods(['setQuoteId']) ->disableOriginalConstructor() ->getMock(); - $this->secureTokenServiceMock = $this->getMockBuilder( + $this->secureTokenService = $this->getMockBuilder( \Magento\Paypal\Model\Payflow\Service\Request\SecureToken::class ) ->setMethods(['requestToken']) ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock = $this->getMockBuilder(\Magento\Framework\Session\SessionManager::class) + $this->sessionManager = $this->getMockBuilder(\Magento\Framework\Session\SessionManager::class) ->setMethods(['getQuote']) ->disableOriginalConstructor() ->getMock(); - $this->transparentMock = $this->getMockBuilder(\Magento\Paypal\Model\Payflow\Transparent::class) - ->setMethods(['getCode']) + $this->transparent = $this->getMockBuilder(\Magento\Paypal\Model\Payflow\Transparent::class) + ->setMethods(['getCode', 'isActive']) ->disableOriginalConstructor() ->getMock(); $this->controller = new \Magento\Paypal\Controller\Transparent\RequestSecureToken( - $this->contextMock, - $this->resultJsonFactoryMock, - $this->sessionTransparentMock, - $this->secureTokenServiceMock, - $this->sessionManagerMock, - $this->transparentMock + $this->context, + $this->resultJsonFactory, + $this->sessionTransparent, + $this->secureTokenService, + $this->sessionManager, + $this->transparent ); } public function testExecuteSuccess() { $quoteId = 99; + $storeId = 2; $tokenFields = ['fields-1', 'fields-2', 'fields-3']; $secureToken = 'token_hash'; $resultExpectation = [ @@ -116,6 +119,8 @@ public function testExecuteSuccess() $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) ->disableOriginalConstructor() ->getMock(); + $quoteMock->method('getStoreId') + ->willReturn($storeId); $tokenMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); @@ -123,21 +128,23 @@ public function testExecuteSuccess() ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) + $this->sessionManager->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); + $this->transparent->method('isActive') + ->with($storeId) + ->willReturn(true); $quoteMock->expects($this->once()) ->method('getId') ->willReturn($quoteId); - $this->sessionTransparentMock->expects($this->once()) + $this->sessionTransparent->expects($this->once()) ->method('setQuoteId') ->with($quoteId); - $this->secureTokenServiceMock->expects($this->once()) + $this->secureTokenService->expects($this->once()) ->method('requestToken') ->with($quoteMock) ->willReturn($tokenMock); - $this->transparentMock->expects($this->once()) - ->method('getCode') + $this->transparent->method('getCode') ->willReturn('transparent'); $tokenMock->expects($this->atLeastOnce()) ->method('getData') @@ -147,7 +154,7 @@ public function testExecuteSuccess() ['securetoken', null, $secureToken] ] ); - $this->resultJsonFactoryMock->expects($this->once()) + $this->resultJsonFactory->expects($this->once()) ->method('create') ->willReturn($jsonMock); $jsonMock->expects($this->once()) @@ -161,6 +168,7 @@ public function testExecuteSuccess() public function testExecuteTokenRequestException() { $quoteId = 99; + $storeId = 2; $resultExpectation = [ 'success' => false, 'error' => true, @@ -170,24 +178,29 @@ public function testExecuteTokenRequestException() $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) ->disableOriginalConstructor() ->getMock(); + $quoteMock->method('getStoreId') + ->willReturn($storeId); $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) + $this->sessionManager->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); $quoteMock->expects($this->once()) ->method('getId') ->willReturn($quoteId); - $this->sessionTransparentMock->expects($this->once()) + $this->transparent->method('isActive') + ->with($storeId) + ->willReturn(true); + $this->sessionTransparent->expects($this->once()) ->method('setQuoteId') ->with($quoteId); - $this->secureTokenServiceMock->expects($this->once()) + $this->secureTokenService->expects($this->once()) ->method('requestToken') ->with($quoteMock) ->willThrowException(new \Exception()); - $this->resultJsonFactoryMock->expects($this->once()) + $this->resultJsonFactory->expects($this->once()) ->method('create') ->willReturn($jsonMock); $jsonMock->expects($this->once()) @@ -211,10 +224,10 @@ public function testExecuteEmptyQuoteError() ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) + $this->sessionManager->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); - $this->resultJsonFactoryMock->expects($this->once()) + $this->resultJsonFactory->expects($this->once()) ->method('create') ->willReturn($jsonMock); $jsonMock->expects($this->once()) diff --git a/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php b/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php index 010c3f8f71de6..82e94462445ae 100644 --- a/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php +++ b/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php @@ -11,6 +11,7 @@ use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Escaper; class BillingAgreementTest extends \PHPUnit\Framework\TestCase { @@ -35,9 +36,16 @@ class BillingAgreementTest extends \PHPUnit\Framework\TestCase */ private $billingAgreement; + /** + * @var Escaper + */ + private $escaperMock; + protected function setUp() { + $helper = new ObjectManager($this); $this->paypalConfig = $this->createMock(Config::class); + $this->escaperMock = $helper->getObject(Escaper::class); $this->paypalConfig ->expects($this->once()) ->method('setMethod') @@ -59,14 +67,13 @@ protected function setUp() ->willReturn($customerId); $this->paypalData = $this->createMock(Data::class); - - $helper = new ObjectManager($this); $this->billingAgreement = $helper->getObject( BillingAgreement::class, [ 'paypalConfigFactory' => $paypalConfigFactory, 'paypalData' => $this->paypalData, - 'currentCustomer' => $this->currentCustomer + 'currentCustomer' => $this->currentCustomer, + 'escaper' => $this->escaperMock ] ); } @@ -83,6 +90,10 @@ public function testGetSectionData() $this->assertArrayHasKey('askToCreate', $result); $this->assertArrayHasKey('confirmUrl', $result); $this->assertArrayHasKey('confirmMessage', $result); + $this->assertEquals( + 'Would you like to sign a billing agreement to streamline further purchases with PayPal?', + $result['confirmMessage'] + ); $this->assertTrue($result['askToCreate']); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php index 78bd269403b83..6bb2173e06f8d 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php @@ -293,6 +293,48 @@ public function testIsMethodActive() $this->config->isMethodActive('method'); } + /** + * Check bill me later active setting uses disable funding options + * + * @param string|null $disableFundingOptions + * @param int $expectedFlag + * @param bool $expectedValue + * + * @dataProvider isMethodActiveBmlDataProvider + */ + public function testIsMethodActiveBml($disableFundingOptions, $expectedFlag, $expectedValue) + { + $this->scopeConfigMock->method('getValue') + ->with( + self::equalTo('payment/paypal_express/disable_funding_options'), + self::equalTo('store') + ) + ->willReturn($disableFundingOptions); + + $this->scopeConfigMock->method('isSetFlag') + ->with('payment/paypal_express_bml/active') + ->willReturn($expectedFlag); + + self::assertEquals($expectedValue, $this->config->isMethodActive('paypal_express_bml')); + } + + /** + * @return array + */ + public function isMethodActiveBmlDataProvider() + { + return [ + ['CREDIT,CARD,ELV', 0, false], + ['CREDIT,CARD,ELV', 1, true], + ['CREDIT', 0, false], + ['CREDIT', 1, true], + ['CARD', 0, true], + ['CARD', 1, true], + [null, 0, true], + [null, 1, true] + ]; + } + /** * Checks a case, when notation code based on Magento edition. */ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php index c1a3a5d5bd999..e7e723f1f3a5f 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php @@ -60,6 +60,7 @@ public function dataProviderExpectedData() 'value' => '0', 'predicate' => [ ], + 'include' => '', ], 'event1' => [ 'value' => '1', @@ -72,6 +73,7 @@ public function dataProviderExpectedData() 'argument2' => 'argument2', ], ], + 'include' => '', ], ], ], @@ -109,6 +111,7 @@ public function dataProviderExpectedData() 'event0' => [ 'value' => '0', 'predicate' => [], + 'include' => '', ], 'event1' => [ 'value' => '1', @@ -121,6 +124,7 @@ public function dataProviderExpectedData() 'argument2' => 'argument2', ], ], + 'include' => '', ], ], ], diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php index 113aa5766ed3f..dd3cf11b87ebe 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php @@ -7,6 +7,7 @@ use Magento\Paypal\Model\Config; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; class ConfigTest extends \PHPUnit\Framework\TestCase { @@ -16,7 +17,7 @@ class ConfigTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ private $scopeConfig; @@ -117,14 +118,29 @@ public function testIsMethodAvailableWPPPE() */ public function testIsMethodAvailableForIsMethodActive($methodName, $expected) { - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->with('paypal/general/merchant_country') - ->will($this->returnValue('US')); - $this->scopeConfig->expects($this->exactly(2)) - ->method('isSetFlag') - ->withAnyParameters() - ->will($this->returnValue(true)); + if ($methodName == Config::METHOD_WPP_BML) { + $valueMap = [ + ['paypal/general/merchant_country', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, 'US'], + ['paypal/general/merchant_country', ScopeInterface::SCOPE_STORE, null, 'US'], + ['payment/paypal_express/disable_funding_options', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, []], + ]; + $this->scopeConfig + ->method('getValue') + ->willReturnMap($valueMap); + $this->scopeConfig->expects($this->exactly(1)) + ->method('isSetFlag') + ->withAnyParameters() + ->willReturn(true); + } else { + $this->scopeConfig + ->method('getValue') + ->with('paypal/general/merchant_country') + ->willReturn('US'); + $this->scopeConfig->expects($this->exactly(2)) + ->method('isSetFlag') + ->withAnyParameters() + ->willReturn(true); + } $this->model->setMethod($methodName); $this->assertEquals($expected, $this->model->isMethodAvailable($methodName)); @@ -219,6 +235,34 @@ public function testGetSpecificConfigPathPayflowAdvancedLink() $this->assertEquals('Authorization', $this->model->getValue('payment_action')); } + /** + * @param string $name + * @param string $expectedValue + * @param string|null $expectedResult + * + * @dataProvider payPalStylesDataProvider + */ + public function testGetSpecificConfigPathPayPalStyles($name, $expectedValue, $expectedResult) + { + // _mapGenericStyleFieldset + $this->scopeConfig->method('getValue') + ->with('paypal/style/' . $name) + ->willReturn($expectedValue); + + $this->assertEquals($expectedResult, $this->model->getValue($name)); + } + + /** + * @return array + */ + public function payPalStylesDataProvider(): array + { + return [ + ['checkout_page_button_customize', 'value', 'value'], + ['test', 'value', null], + ]; + } + /** * @dataProvider skipOrderReviewStepDataProvider */ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php index 935b4484a8d20..b316f92c0ce85 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php @@ -5,7 +5,10 @@ */ namespace Magento\Paypal\Test\Unit\Model; +use Magento\Framework\UrlInterface; use Magento\Paypal\Model\ExpressConfigProvider; +use Magento\Paypal\Model\SmartButtonConfig; +use PHPUnit\Framework\MockObject\MockObject; class ExpressConfigProviderTest extends \PHPUnit\Framework\TestCase { @@ -40,16 +43,19 @@ public function testGetConfig() $payment->expects($this->atLeastOnce())->method('getCheckoutRedirectUrl')->willReturn('http://redirect.url'); $paymentHelper->expects($this->atLeastOnce())->method('getMethodInstance')->willReturn($payment); - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject $urlBuilderMock */ + /** @var UrlInterface|MockObject $urlBuilderMock */ $urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); + $smartButtonConfigMock = $this->createMock(SmartButtonConfig::class); + $configProvider = new ExpressConfigProvider( $configFactory, $localeResolver, $currentCustomer, $paypalHelper, $paymentHelper, - $urlBuilderMock + $urlBuilderMock, + $smartButtonConfigMock ); $configProvider->getConfig(); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php index 80c8194e07654..5ac436bcf0a3a 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php @@ -131,7 +131,7 @@ public function testInitialize() ->method('postRequest') ->willReturn($response); - $this->payflowRequest->expects($this->exactly(3)) + $this->payflowRequest->expects($this->exactly(4)) ->method('setData') ->willReturnMap( [ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php b/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php index 3e8af6b2ee766..7c352fc497a38 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php @@ -580,6 +580,40 @@ public function testPostRequestException() $this->payflowpro->postRequest($request, $config); } + /** + * @covers \Magento\Paypal\Model\Payflowpro::addRequestOrderInfo + */ + public function testAddRequestOrderInfo() + { + $orderData = [ + 'id' => 1, + 'increment_id' => '0000001' + ]; + $data = [ + 'ponum' => $orderData['id'], + 'custref' => $orderData['increment_id'], + 'invnum' => $orderData['increment_id'], + 'comment1' => $orderData['increment_id'] + ]; + $expectedData = new DataObject($data); + $actualData = new DataObject(); + + $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->setMethods(['getIncrementId', 'getId']) + ->getMock(); + $orderMock->expects(static::once()) + ->method('getId') + ->willReturn($orderData['id']); + $orderMock->expects(static::atLeastOnce()) + ->method('getIncrementId') + ->willReturn($orderData['increment_id']); + + $this->payflowpro->addRequestOrderInfo($actualData, $orderMock); + + $this->assertEquals($expectedData, $actualData); + } + /** * @covers \Magento\Paypal\Model\Payflowpro::assignData */ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php new file mode 100644 index 0000000000000..ed62efe36c472 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Test\Unit\Model; + +use Magento\Paypal\Model\SmartButtonConfig; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Paypal\Model\ConfigFactory; + +class SmartButtonConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Paypal\Model\SmartButtonConfig + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $localeResolverMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + protected function setUp() + { + $this->localeResolverMock = $this->getMockForAbstractClass(ResolverInterface::class); + $this->configMock = $this->getMockBuilder(\Magento\Paypal\Model\Config::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var \PHPUnit_Framework_MockObject_MockObject $configFactoryMock */ + $configFactoryMock = $this->getMockBuilder(ConfigFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $configFactoryMock->expects($this->once())->method('create')->willReturn($this->configMock); + $this->model = new SmartButtonConfig( + $this->localeResolverMock, + $configFactoryMock, + $this->getDefaultStyles(), + $this->getAllowedFundings() + ); + } + + /** + * @param string $page + * @param string $locale + * @param string $disallowedFundings + * @param string $layout + * @param string $size + * @param string $shape + * @param string $label + * @param string $color + * @param string $installmentPeriodLabel + * @param string $installmentPeriodLocale + * @param array $expected + * @dataProvider getConfigDataProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function testGetConfig( + string $page, + string $locale, + bool $isCustomize, + ?string $disallowedFundings, + string $layout, + string $size, + string $shape, + string $label, + string $color, + string $installmentPeriodLabel, + string $installmentPeriodLocale, + array $expected = [] + ) { + $this->localeResolverMock->expects($this->any())->method('getLocale')->willReturn($locale); + $this->configMock->expects($this->any())->method('getValue')->will($this->returnValueMap([ + ['merchant_id', null, 'merchant'], + ['sandbox_flag', null, true], + ['disable_funding_options', null, $disallowedFundings], + ["{$page}_page_button_customize", null, $isCustomize], + ["{$page}_page_button_layout", null, $layout], + ["{$page}_page_button_size", null, $size], + ["{$page}_page_button_color", null, $color], + ["{$page}_page_button_shape", null, $shape], + ["{$page}_page_button_label", null, $label], + [$page . '_page_button_' . $installmentPeriodLocale . '_installment_period', null, $installmentPeriodLabel] + ])); + + self::assertEquals($expected, $this->model->getConfig($page)); + } + + public function getConfigDataProvider() + { + return include __DIR__ . '/_files/expected_config.php'; + } + + /** + * @return array + */ + private function getDefaultStyles() + { + return include __DIR__ . '/_files/default_styles.php'; + } + + /** + * @return array + */ + private function getAllowedFundings() + { + return include __DIR__ . '/_files/allowed_fundings.php'; + } +} diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php new file mode 100644 index 0000000000000..6b6f8ccb87e14 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'checkout' => [ + 'CREDIT', + 'ELV' + ], + 'cart' => [ + 'CREDIT', + 'ELV' + ], + 'mini_cart' => [ + 'CREDIT', + 'ELV' + ], + 'product' => [ + 'CREDIT' + ] +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/default_styles.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/default_styles.php new file mode 100644 index 0000000000000..87da99ed2e178 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/default_styles.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'checkout' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' =>'paypal' + ], + 'cart' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' =>'paypal' + ], + 'mini_cart' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' =>'paypal' + ], + 'product' => [ + 'layout' => 'horizontal', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'pill', + 'label' =>'buynow' + ] +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php new file mode 100644 index 0000000000000..3a76d11e51374 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'cart' => [ + 'cart', + 'es_MX', + true, + 'CREDIT', + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'mx', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'es_MX', + 'allowedFunding' => ['ELV'], + 'disallowedFunding' => ['CREDIT'], + 'styles' => [ + 'layout' => 'horizontal', + 'size' => 'small', + 'color' => 'blue', + 'shape' => 'pillow', + 'label' => 'installment', + 'installmentperiod' => 0 + ] + ] + ], + 'checkout' => [ + 'cart', + 'en_BR', + true, + null, + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en_BR', + 'allowedFunding' => ['CREDIT', 'ELV'], + 'disallowedFunding' => [], + 'styles' => [ + 'layout' => 'horizontal', + 'size' => 'small', + 'color' => 'blue', + 'shape' => 'pillow', + 'label' => 'installment', + 'installmentperiod' => 0 + ] + ] + ], + 'mini_cart' => [ + 'cart', + 'en', + false, + null, + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en', + 'allowedFunding' => ['CREDIT', 'ELV'], + 'disallowedFunding' => [], + 'styles' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' => 'paypal' + ] + ] + ], + 'mini_cart' => [ + 'cart', + 'en', + false, + null, + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en', + 'allowedFunding' => ['CREDIT', 'ELV'], + 'disallowedFunding' => [], + 'styles' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' => 'paypal' + ] + ] + ], + 'product' => [ + 'cart', + 'en', + false, + 'CREDIT', + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en', + 'allowedFunding' => ['ELV'], + 'disallowedFunding' => ['CREDIT'], + 'styles' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' => 'paypal', + ] + ] + ] +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php b/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php index 7cb521073e309..542b327475de1 100644 --- a/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php @@ -9,9 +9,9 @@ use Magento\Catalog\Block\ShortcutInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\Observer; -use Magento\Framework\View\Element\Template; use Magento\Framework\View\Layout; -use Magento\Paypal\Block\Express\InContext\Minicart\Button; +use Magento\Paypal\Block\Express\InContext\Minicart\SmartButton as MinicartButton; +use Magento\Paypal\Block\Express\InContext\SmartButton as Button; use Magento\Paypal\Helper\Shortcut\Factory; use Magento\Paypal\Model\Config; use Magento\Paypal\Observer\AddPaypalShortcutsObserver; @@ -119,7 +119,7 @@ public function testAddShortcutsButtons(array $blocks) ++$callIndexSession; } - $blockMock = $this->getMockBuilder(Button::class) + $blockMock = $this->getMockBuilder(MinicartButton::class) ->setMethods(['setIsInCatalogProduct', 'setShowOrPosition']) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -159,7 +159,12 @@ public function dataProviderShortcutsButtons() return [ [ 'blocks1' => [ - \Magento\Paypal\Block\Express\InContext\Minicart\Button::class => [ + MinicartButton::class => [ + self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, + self::PAYMENT_AVAILABLE => true, + self::PAYMENT_IS_BML => false, + ], + Button::class => [ self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, self::PAYMENT_AVAILABLE => true, self::PAYMENT_IS_BML => false, @@ -198,11 +203,16 @@ public function dataProviderShortcutsButtons() ], [ 'blocks2' => [ - \Magento\Paypal\Block\Express\InContext\Minicart\Button::class => [ + MinicartButton::class => [ self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, self::PAYMENT_AVAILABLE => false, self::PAYMENT_IS_BML => false, ], + Button::class => [ + self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, + self::PAYMENT_AVAILABLE => true, + self::PAYMENT_IS_BML => false, + ], \Magento\Paypal\Block\Express\Shortcut::class => [ self::PAYMENT_CODE => Config::METHOD_WPP_EXPRESS, self::PAYMENT_AVAILABLE => false, diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml index 99a6668c0153f..cbb95e376c9f4 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml @@ -132,6 +132,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml index a8aac92fccd6a..51297a96438d2 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml @@ -207,6 +207,7 @@ <rule type="paypalExpressLockConfigurationConditional" event=":load"> <argument name="payflow_link_ca">payflow_link_ca</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml index fd570c9822f25..46c61b52b75dc 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml @@ -26,6 +26,7 @@ <rule type="inContextActivate" event="activate-in-context-api"/> <rule type="inContextDeactivate" event="deactivate-in-context-api"/> <rule type="inContextDisableConditional" event=":load"/> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml index fd2bcb266763c..28cc075e0c619 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml index da0c1bd635347..7f1fcc08334fe 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml index a809817fe9b3d..d8b765b9b4d22 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml @@ -15,7 +15,7 @@ <predicate name="confirm" message="There is already another PayPal solution enabled. Enable this solution instead?" event="deactivate-rule" - > + > <argument name="wps_express">wps_express</argument> </predicate> </event> @@ -39,16 +39,16 @@ <predicate name="confirm" message="There is already another PayPal solution enabled. Enable this solution instead?" event="deactivate-rule" - > + > <argument name="payments_pro_hosted_solution_with_express_checkout">payments_pro_hosted_solution_with_express_checkout</argument> - <argument name="express_checkout_us">express_checkout_us</argument> + <argument name="express_checkout_gb">express_checkout_gb</argument> </predicate> </event> </events> <relation target="payments_pro_hosted_solution_with_express_checkout"> <rule type="disable" event="activate-rule"/> </relation> - <relation target="express_checkout_us"> + <relation target="express_checkout_gb"> <rule type="disable" event="activate-rule"/> </relation> <relation target=":self"> @@ -56,19 +56,19 @@ <rule type="simpleDisable" event="deactivate-rule"/> <rule type="conflict" event=":load"> <argument name="payments_pro_hosted_solution_with_express_checkout">payments_pro_hosted_solution_with_express_checkout</argument> - <argument name="express_checkout_us">express_checkout_us</argument> + <argument name="express_checkout_gb">express_checkout_gb</argument> </rule> </relation> </payment> <!-- Express Checkout --> - <payment id="express_checkout_us"> + <payment id="express_checkout_gb"> <events selector="[data-enable='payment']"> <event value="0" name="deactivate-rule"/> <event value="1" name="activate-rule"> <predicate name="confirm" message="There is already another PayPal solution enabled. Enable this solution instead?" event="deactivate-rule" - > + > <argument name="wps_express">wps_express</argument> </predicate> </event> @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_express">wps_express</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> -</rules> +</rules> \ No newline at end of file diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml index 1c5dbaf22f977..50ce14e66ee0c 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml index 2fe4ad78d4bff..de059dcc59c39 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml index e6fe55aa90493..d9fc7ef3f201c 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml index 79309bcee7015..c5b8b09c3a2cf 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml index a4118cc964fc6..972cc45505ecb 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml @@ -64,6 +64,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml index 02cb608c07c8a..b7924e770aa22 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml @@ -396,6 +396,10 @@ <event value="0" name="deactivate-in-context-api"/> <event value="1" name="activate-in-context-api"/> </events> + <events selector="[data-enable='disable-funding-options']"> + <event value="CREDIT" include="true" name="remove-option"/> + <event value="CREDIT" include="false" name="add-option"/> + </events> <relation target="wps_express"> <rule type="disable" event="activate-rule"/> </relation> @@ -433,6 +437,9 @@ <argument name="paypal_payflowpro_with_express_checkout">paypal_payflowpro_with_express_checkout</argument> <argument name="payflow_link_us">payflow_link_us</argument> </rule> + <rule type="removeCreditOption" event="remove-option"/> + <rule type="addCreditOption" event="add-option"/> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system.xml b/app/code/Magento/Paypal/etc/adminhtml/system.xml index 26ec9b3152e4b..ea48aa65132e8 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system.xml @@ -106,12 +106,23 @@ <field id="enable_express_checkout"> <config_path>payment/wps_express/active</config_path> </field> - <field id="enable_express_checkout_bml"> + <field id="enable_express_checkout_bml" showInDefault="1" showInWebsite="1"> <config_path>payment/wps_express_bml/active</config_path> </field> + <field id="express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"/> </group> <group id="settings_ec" translate="label"> <label>Basic Settings - PayPal Website Payments Standard</label> + <group id="settings_ec_advanced"> + <group id="express_checkout_frontend"> + <field id="checkout_display" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="checkout_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="product_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="cart_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="mini_cart_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="features" showInDefault="0" showInWebsite="0" showInStore="0"/> + </group> + </group> </group> </group> </group> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml index bff076aad9cb5..7abefbe1a674e 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml @@ -158,7 +158,7 @@ </depends> <validate>required-entry</validate> </field> - <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="1" showInWebsite="1"> + <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="0" showInWebsite="0"> <label>Enable PayPal Credit</label> <comment><![CDATA[PayPal Express Checkout lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. @@ -171,7 +171,7 @@ <field id="enable_express_checkout"/> </requires> </field> - <field id="express_checkout_bml_sort_order" translate="label" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="express_checkout_bml_sort_order" translate="label" type="text" sortOrder="25" showInDefault="0" showInWebsite="0" showInStore="0"> <label>Sort Order PayPal Credit</label> <config_path>payment/paypal_express_bml/sort_order</config_path> <frontend_class>validate-number</frontend_class> @@ -645,6 +645,262 @@ </tooltip> <attribute type="shared">1</attribute> </field> + <field id="checkout_display" translate="label" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Customize Smart Buttons</label> + <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> + <attribute type="shared">1</attribute> + </field> + <group id="checkout_page_button" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> + <label>Checkout Page</label> + <field id="checkout_page_button_customize" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="10"> + <label>Customize Button</label> + <config_path>paypal/style/checkout_page_button_customize</config_path> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_label" translate="label comment" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> + <label>Label</label> + <comment><![CDATA[The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR.]]></comment> + <config_path>paypal/style/checkout_page_button_label</config_path> + <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\ButtonStylesLabel</frontend_model> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getLabel</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_mx_installment_period" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> + <label>Mexico Installment Period</label> + <config_path>paypal/style/checkout_page_button_mx_installment_period</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getMxInstallmentPeriod</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label">installment</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_br_installment_period" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> + <label>Brazil Installment Period</label> + <config_path>paypal/style/checkout_page_button_br_installment_period</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getBrInstallmentPeriod</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label">installment</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_layout" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> + <label>Layout</label> + <config_path>paypal/style/checkout_page_button_layout</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getLayout</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label" negative="1">credit</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_size" translate="label tooltip" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> + <label>Size</label> + <config_path>paypal/style/checkout_page_button_size</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getSize</source_model> + <tooltip>Select Responsive to ensure the PayPal button renders correctly on mobile devices.</tooltip> + <depends> + <field id="checkout_page_button_customize">1</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_shape" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> + <label>Shape</label> + <config_path>paypal/style/checkout_page_button_shape</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getShape</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_color" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> + <label>Color</label> + <config_path>paypal/style/checkout_page_button_color</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getColor</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label" negative="1">credit</field> + </depends> + <attribute type="shared">1</attribute> + </field> + </group> + <group id="product_page_button" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> + <label>Product Pages</label> + <field id="product_page_button_customize" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_customize"> + <config_path>paypal/style/product_page_button_customize</config_path> + </field> + <field id="product_page_button_label" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_label"> + <config_path>paypal/style/product_page_button_label</config_path> + <depends> + <field id="product_page_button_customize">1</field> + </depends> + </field> + <field id="product_page_button_mx_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_mx_installment_period"> + <config_path>paypal/style/product_page_button_mx_installment_period</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label">installment</field> + </depends> + </field> + <field id="product_page_button_br_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_br_installment_period"> + <config_path>paypal/style/product_page_button_br_installment_period</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label">installment</field> + </depends> + </field> + <field id="product_page_button_layout" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_layout"> + <config_path>paypal/style/product_page_button_layout</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label" negative="1">credit</field> + </depends> + </field> + <field id="product_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> + <config_path>paypal/style/product_page_button_size</config_path> + <depends> + <field id="product_page_button_customize">1</field> + </depends> + </field> + <field id="product_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> + <config_path>paypal/style/product_page_button_shape</config_path> + <depends> + <field id="product_page_button_customize">1</field> + </depends> + </field> + <field id="product_page_button_color" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_color"> + <config_path>paypal/style/product_page_button_color</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label" negative="1">credit</field> + </depends> + </field> + </group> + <group id="cart_page_button" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="110"> + <label>Cart Page</label> + <field id="cart_page_button_customize" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_customize"> + <config_path>paypal/style/cart_page_button_customize</config_path> + </field> + <field id="cart_page_button_label" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_label"> + <config_path>paypal/style/cart_page_button_label</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + </depends> + </field> + <field id="cart_page_button_mx_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_mx_installment_period"> + <config_path>paypal/style/cart_page_button_mx_installment_period</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label">installment</field> + </depends> + </field> + <field id="cart_page_button_br_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_br_installment_period"> + <config_path>paypal/style/cart_page_button_br_installment_period</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label">installment</field> + </depends> + </field> + <field id="cart_page_button_layout" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_layout"> + <config_path>paypal/style/cart_page_button_layout</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label" negative="1">credit</field> + </depends> + </field> + <field id="cart_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> + <config_path>paypal/style/cart_page_button_size</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + </depends> + </field> + <field id="cart_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> + <config_path>paypal/style/cart_page_button_shape</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + </depends> + </field> + <field id="cart_page_button_color" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_color"> + <config_path>paypal/style/cart_page_button_color</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label" negative="1">credit</field> + </depends> + </field> + </group> + <group id="mini_cart_page_button" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="120"> + <label>Mini Cart</label> + <field id="mini_cart_page_button_customize" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_customize"> + <config_path>paypal/style/mini_cart_page_button_customize</config_path> + </field> + <field id="mini_cart_page_button_label" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_label"> + <config_path>paypal/style/mini_cart_page_button_label</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + </depends> + </field> + <field id="mini_cart_page_button_mx_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_mx_installment_period"> + <config_path>paypal/style/mini_cart_page_button_mx_installment_period</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label">installment</field> + </depends> + </field> + <field id="mini_cart_page_button_br_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_br_installment_period"> + <config_path>paypal/style/mini_cart_page_button_br_installment_period</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label">installment</field> + </depends> + </field> + <field id="mini_cart_page_button_layout" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_layout"> + <config_path>paypal/style/mini_cart_page_button_layout</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label" negative="1">credit</field> + </depends> + </field> + <field id="mini_cart_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> + <config_path>paypal/style/mini_cart_page_button_size</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + </depends> + </field> + <field id="mini_cart_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> + <config_path>paypal/style/mini_cart_page_button_shape</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + </depends> + </field> + <field id="mini_cart_page_button_color" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_color"> + <config_path>paypal/style/mini_cart_page_button_color</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label" negative="1">credit</field> + </depends> + </field> + </group> + <group id="features" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="130"> + <label>Features</label> + <field id="disable_funding_options" translate="label comment" type="multiselect" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Disable Funding Options</label> + <comment> + <![CDATA[PayPal will automatically display each enabled funding option to eligible buyers. + For example, PayPal Credit is only shown to buyers in countries where PayPal Credit is + offered and the currency offered by the merchant is USD.]]> + </comment> + <config_path>paypal/style/disable_funding_options</config_path> + <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\MultiSelect\DisabledFundingOptions</frontend_model> + <source_model>Magento\Paypal\Model\System\Config\Source\DisableFundingOptions</source_model> + <attribute type="shared">1</attribute> + <can_be_empty>1</can_be_empty> + </field> + </group> </group> </group> </group> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml index 5eb596c9c4f45..e7de9c0d641a7 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml @@ -97,7 +97,7 @@ <field id="enable_payflow_advanced"/> </requires> </field> - <field id="enable_express_checkout_bml" sortOrder="42" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml"> + <field id="enable_express_checkout_bml" sortOrder="42" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" showInDefault="1" showInWebsite="1"> <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> @@ -108,7 +108,7 @@ <field id="enable_payflow_advanced"/> </requires> </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> + <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"> <config_path>payment/payflow_express_bml/sort_order</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> <depends> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml index d27dde02c579e..647bc7a60975a 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml @@ -106,7 +106,7 @@ <field id="enable_payflow_link"/> </requires> </field> - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="41"> + <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="41" showInDefault="1" showInWebsite="1"> <comment><![CDATA[Payflow Link lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> @@ -117,7 +117,7 @@ <field id="enable_express_checkout"/> </requires> </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> + <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"> <config_path>payment/payflow_express_bml/sort_order</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> <depends> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml index 77acff48c247e..35cd844204843 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml @@ -46,7 +46,7 @@ <frontend_class>paypal-enabler paypal-ec-separate</frontend_class> </field> - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="21"> + <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="21" showInDefault="1" showInWebsite="1"> <comment><![CDATA[Payments Pro Hosted Solution lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml index 6090025024dd7..694e517816b22 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml @@ -6,7 +6,7 @@ */ --> <include xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_include.xsd"> - <group id="paypal_payflowpro_with_express_checkout" translate="label comment" extends="payment_all_paypal/paypal_payflowpro"> + <group id="paypal_payflowpro_with_express_checkout" translate="label" extends="payment_all_paypal/paypal_payflowpro"> <label>Payflow Pro</label> <attribute type="paypal_ec_separate">0</attribute> <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> @@ -30,7 +30,7 @@ <field id="enable_paypal_payflow"/> </requires> </field> - <field id="enable_express_checkout_bml_payflow" translate="label" type="select" sortOrder="21" showInWebsite="1" showInDefault="1"> + <field id="enable_express_checkout_bml_payflow" translate="label comment" type="select" sortOrder="21" showInWebsite="1" showInDefault="1"> <label>Enable PayPal Credit</label> <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. @@ -43,7 +43,7 @@ <field id="enable_paypal_payflow"/> </requires> </field> - <field id="express_checkout_bml_sort_order" sortOrder="30" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> + <field id="express_checkout_bml_sort_order" sortOrder="30" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"> <config_path>payment/payflow_express_bml/sort_order</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> <depends> diff --git a/app/code/Magento/Paypal/etc/config.xml b/app/code/Magento/Paypal/etc/config.xml index 5fc457fb4443d..1880417af1b48 100644 --- a/app/code/Magento/Paypal/etc/config.xml +++ b/app/code/Magento/Paypal/etc/config.xml @@ -10,6 +10,30 @@ <paypal> <style> <logo></logo> + <checkout_page_button_customize>0</checkout_page_button_customize> + <checkout_page_button_label>paypal</checkout_page_button_label> + <checkout_page_button_layout>vertical</checkout_page_button_layout> + <checkout_page_button_size>responsive</checkout_page_button_size> + <checkout_page_button_shape>rect</checkout_page_button_shape> + <checkout_page_button_color>gold</checkout_page_button_color> + <product_page_button_customize>0</product_page_button_customize> + <product_page_button_label>buynow</product_page_button_label> + <product_page_button_layout>horizontal</product_page_button_layout> + <product_page_button_size>responsive</product_page_button_size> + <product_page_button_shape>pill</product_page_button_shape> + <product_page_button_color>gold</product_page_button_color> + <cart_page_button_customize>0</cart_page_button_customize> + <cart_page_button_label>paypal</cart_page_button_label> + <cart_page_button_layout>vertical</cart_page_button_layout> + <cart_page_button_size>responsive</cart_page_button_size> + <cart_page_button_shape>rect</cart_page_button_shape> + <cart_page_button_color>gold</cart_page_button_color> + <mini_cart_page_button_customize>0</mini_cart_page_button_customize> + <mini_cart_page_button_label>paypal</mini_cart_page_button_label> + <mini_cart_page_button_layout>vertical</mini_cart_page_button_layout> + <mini_cart_page_button_size>responsive</mini_cart_page_button_size> + <mini_cart_page_button_shape>rect</mini_cart_page_button_shape> + <mini_cart_page_button_color>gold</mini_cart_page_button_color> </style> <wpp> <api_password backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> diff --git a/app/code/Magento/Paypal/etc/frontend/di.xml b/app/code/Magento/Paypal/etc/frontend/di.xml index 407c251fc42f4..8c29ae1e2685f 100644 --- a/app/code/Magento/Paypal/etc/frontend/di.xml +++ b/app/code/Magento/Paypal/etc/frontend/di.xml @@ -86,7 +86,7 @@ </argument> </arguments> </type> - <type name="Magento\Paypal\Block\Express\InContext\Minicart\Button"> + <type name="Magento\Paypal\Block\Express\InContext\Minicart\SmartButton"> <arguments> <argument name="data" xsi:type="array"> <item name="template" xsi:type="string">Magento_Paypal::express/in-context/shortcut/button.phtml</item> @@ -97,6 +97,14 @@ <argument name="payment" xsi:type="object">Magento\Paypal\Model\Express</argument> </arguments> </type> + <type name="Magento\Paypal\Block\Express\InContext\SmartButton"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="alias" xsi:type="string">product.info.addtocart.paypalexpress</item> + <item name="template" xsi:type="string">express/shortcut_button.phtml</item> + </argument> + </arguments> + </type> <type name="Magento\Vault\Model\Ui\TokensConfigProvider"> @@ -116,4 +124,56 @@ <type name="Magento\Quote\Model\QuoteRepository\SaveHandler"> <plugin name="paypal-cartitem" type="Magento\Paypal\Model\Express\QuotePlugin"/> </type> + + <type name="Magento\Paypal\Model\SmartButtonConfig"> + <arguments> + <argument name="defaultStyles" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="layout" xsi:type="string">vertical</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">rect</item> + <item name="label" xsi:type="string">paypal</item> + </item> + <item name="cart" xsi:type="array"> + <item name="layout" xsi:type="string">vertical</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">rect</item> + <item name="label" xsi:type="string">paypal</item> + </item> + <item name="mini_cart" xsi:type="array"> + <item name="layout" xsi:type="string">vertical</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">rect</item> + <item name="label" xsi:type="string">paypal</item> + </item> + <item name="product" xsi:type="array"> + <item name="layout" xsi:type="string">horizontal</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">pill</item> + <item name="label" xsi:type="string">buynow</item> + </item> + </argument> + <argument name="allowedFunding" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + <item name="1" xsi:type="string">ELV</item> + </item> + <item name="cart" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + <item name="1" xsi:type="string">ELV</item> + </item> + <item name="mini_cart" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + <item name="1" xsi:type="string">ELV</item> + </item> + <item name="product" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Paypal/etc/frontend/sections.xml b/app/code/Magento/Paypal/etc/frontend/sections.xml index 466b930fb9bee..ca32d57cd9d28 100644 --- a/app/code/Magento/Paypal/etc/frontend/sections.xml +++ b/app/code/Magento/Paypal/etc/frontend/sections.xml @@ -15,4 +15,8 @@ <section name="cart"/> <section name="checkout-data"/> </action> + <action name="paypal/express/onAuthorization"> + <section name="cart"/> + <section name="checkout-data"/> + </action> </config> diff --git a/app/code/Magento/Paypal/etc/rules.xsd b/app/code/Magento/Paypal/etc/rules.xsd index 9a274a2dbd1cc..c4385396cc0c9 100644 --- a/app/code/Magento/Paypal/etc/rules.xsd +++ b/app/code/Magento/Paypal/etc/rules.xsd @@ -31,6 +31,7 @@ </xs:sequence> <xs:attribute type="xs:string" name="value" use="required"/> <xs:attribute type="xs:string" name="name" use="required"/> + <xs:attribute type="xs:boolean" name="include" use="optional"/> </xs:complexType> <xs:complexType name="predicate"> <xs:sequence minOccurs="0" maxOccurs="unbounded"> diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index c3f3e38fa03b4..e7264a6de807f 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -697,3 +697,43 @@ User,User The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% or more. <a href=""https://financing.paypal.com/ppfinportal/content/forrester"" target=""_blank"">See Details</a>. " +"PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. + You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. + <a href=""https://www.paypal.com/webapps/mpp/promotional-financing"" target=""_blank"">Learn More</a>","PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. + You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. + <a href=""https://www.paypal.com/webapps/mpp/promotional-financing"" target=""_blank"">Learn More</a>" +"Customize Smart Buttons","Customize Smart Buttons" +"Checkout Page","Checkout Page" +"Label","Label" +"The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR.","The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR." +"Checkout","Checkout" +"Credit","Credit" +"Pay","Pay" +"Buy Now","Buy Now" +"PayPal","PayPal" +"Installment","Installment" +"Mexico Installment Period","Mexico Installment Period" +"Brazil Installment Period","Brazil Installment Period" +"Layout","Layout" +"Vertical","Vertical" +"Horizontal","Horizontal" +"Size","Size" +"Medium","Medium" +"Large","Large" +"Responsive","Responsive" +"Shape","Shape" +"Pill","Pill" +"Rectangle","Rectangle" +"Color","Color" +"Gold","Gold" +"Blue","Blue" +"Silver","Silver" +"Black","Black" +"Product Pages","Product Pages" +"Cart Page","Cart Page" +"Mini Cart","Mini Cart" +"Features","Features" +"PayPal will automatically display each enabled funding option to eligible buyers. For example, PayPal Credit is only shown to buyers in countries where PayPal Credit is offered and the currency offered by the merchant is USD.","PayPal will automatically display each enabled funding option to eligible buyers. For example, PayPal Credit is only shown to buyers in countries where PayPal Credit is offered and the currency offered by the merchant is USD." +"PayPal Credit","PayPal Credit" +"PayPal Guest Checkout Credit Card Icons","PayPal Guest Checkout Credit Card Icons" +"Elektronisches Lastschriftverfahren - German ELV","Elektronisches Lastschriftverfahren - German ELV" diff --git a/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js b/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js index 4a5248cb87587..555d2a80a8610 100644 --- a/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js +++ b/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js @@ -585,6 +585,41 @@ define([ 'Please re-enable the previously enabled payment solutions.' }); } + }, + + /** + * @param {*} $target + * @param {*} $owner + * @param {Object} data + */ + removeCreditOption: function ($target, $owner, data) { + if ($target.find(data.dependsButtonLabel + ' option[value="credit"]').length > 0) { + $target.find(data.dependsButtonLabel + ' option[value="credit"]').remove(); + } + }, + + /** + * @param {*} $target + * @param {*} $owner + * @param {Object} data + */ + addCreditOption: function ($target, $owner, data) { + if ($target.find(data.dependsButtonLabel + ' option[value="credit"]').length === 0) { + $target.find(data.dependsButtonLabel).append('<option value="credit">Credit</option>'); + } + }, + + /** + * @param {*} $target + * @param {*} $owner + * @param {Object} data + */ + removeCreditOptionConditional: function ($target, $owner, data) { + if ($target.find(data.dependsDisableFundingOptions + ' option[value="CREDIT"]').length === 0 || + $target.find(data.dependsDisableFundingOptions + ' option[value="CREDIT"]:selected').length > 0 + ) { + this.removeCreditOption($target, $owner, data); + } } }); }); diff --git a/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js b/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js index 3d832db09aa87..3e4a1ab0ccc75 100644 --- a/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js +++ b/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js @@ -65,6 +65,18 @@ define([ */ dependsBmlApiSortOrder: '[data-enable="bml-api-sort-order"]', + /** + * An attribute of the element responsible for the visibility of the + * button Label credit option (data attribute) + */ + dependsButtonLabel: '[data-enable="button-label"]', + + /** + * An attribute of the element responsible for the visibility of the + * button Label credit option on load (data attribute) + */ + dependsDisableFundingOptions: '[data-enable="disable-funding-options"]', + /** * Templates element selectors */ @@ -119,7 +131,9 @@ define([ } }; - if (solution.getValue($(this)) === elementEvent.value) { + if (solution.getValue($(this)) === elementEvent.value || + $(this).prop('multiple') && solution.checkMultiselectValue($(this), elementEvent) + ) { if (predicate.name) { require([ 'Magento_Paypal/js/predicate/' + predicate.name @@ -147,6 +161,23 @@ define([ return $element.val(); }, + /** + * Check multiselect value based on include value + * + * @param {Object} $element + * @param {Object} elementEvent + * @returns {Boolean} + */ + checkMultiselectValue: function ($element, elementEvent) { + var isValueSelected = $.inArray(elementEvent.value, $element.val()) >= 0; + + if (elementEvent.include) { + isValueSelected = (isValueSelected ? 'true' : 'false') === elementEvent.include; + } + + return isValueSelected; + }, + /** * Adding event listeners * @@ -175,6 +206,8 @@ define([ dependsMerchantId: this.dependsMerchantId, dependsBmlSortOrder: this.dependsBmlSortOrder, dependsBmlApiSortOrder: this.dependsBmlApiSortOrder, + dependsButtonLabel: this.dependsButtonLabel, + dependsDisableFundingOptions: this.dependsDisableFundingOptions, solutionsElements: this.solutionsElements, argument: instance.argument } diff --git a/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml index 73c44faff5a57..ebf38dd2d9945 100644 --- a/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml @@ -47,9 +47,6 @@ <item name="paypal_express" xsi:type="array"> <item name="isBillingAddressRequired" xsi:type="boolean">false</item> </item> - <item name="paypal_express_bml" xsi:type="array"> - <item name="isBillingAddressRequired" xsi:type="boolean">false</item> - </item> <item name="payflow_express_bml" xsi:type="array"> <item name="isBillingAddressRequired" xsi:type="boolean">false</item> </item> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml index 3725f51f0b8bb..66dddfb0bda95 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml @@ -4,26 +4,10 @@ * See COPYING.txt for license details. */ -use Magento\Paypal\Block\Express\InContext\Minicart\Button; - /** - * @var \Magento\Paypal\Block\Express\InContext\Minicart\Button $block + * @var \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton $block */ -$config = [ - 'Magento_Paypal/js/in-context/button' => [ - 'id' => $block->escapeHtml($block->getContainerId()), - 'linkDataAction' => $block->escapeHtml($block->getLinkAction()), - 'paypalButton' => $block->escapeHtml(Button::PAYPAL_BUTTON_ID), - 'addToCartSelector' => $block->escapeHtml($block->getAddToCartSelector()) - ] -]; - ?> -<div data-mage-init='<?= /* @noEscape */ json_encode($config) ?>' +<div data-mage-init='<?= /* @noEscape */ $block->getJsInitParams() ?>' class="paypal checkout paypal-logo <?= $block->escapeHtml($block->getContainerId()) ?>-container"> - <a data-action="<?= $block->escapeHtml($block->getLinkAction()) ?>" href="#"> - <img class="paypal-button-hidden" - src="<?= $block->escapeHtml($block->getImageUrl()) ?>" - alt="Check out with PayPal" /> - </a> -</div> +</div> \ No newline at end of file diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml new file mode 100644 index 0000000000000..ac0eda99ee939 --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml @@ -0,0 +1,13 @@ + +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @var \Magento\Paypal\Block\Express\Shortcut $block + */ +?> +<div data-mage-init='<?= /* @noEscape */ $block->getJsInitParams() ?>'></div> \ No newline at end of file diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js index 8b4855cff6853..012a1f18f9ae5 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js @@ -2,50 +2,52 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -define( - [ - 'uiComponent', - 'jquery', - 'domReady!' - ], - function ( - Component, - $ - ) { - 'use strict'; - - return Component.extend({ - - defaults: {}, - - /** - * @returns {Object} - */ - initialize: function () { - this._super(); - - return this.initEvents(); - }, - - /** - * @returns {Object} - */ - initEvents: function () { - $('a[data-action="' + this.linkDataAction + '"]').off('click.' + this.id) - .on('click.' + this.id, this.click.bind(this)); - - return this; - }, - - /** - * @param {Object} event - * @returns void - */ - click: function (event) { - event.preventDefault(); - - $('#' + this.paypalButton).click(); +define([ + 'uiComponent', + 'jquery', + 'Magento_Paypal/js/in-context/express-checkout-wrapper', + 'Magento_Customer/js/customer-data' +], function (Component, $, Wrapper, customerData) { + 'use strict'; + + return Component.extend(Wrapper).extend({ + defaults: { + declinePayment: false + }, + + /** @inheritdoc */ + initialize: function (config, element) { + var cart = customerData.get('cart'), + customer = customerData.get('customer'); + + this._super(); + this.renderPayPalButtons(element); + this.declinePayment = !customer().firstname && !cart().isGuestCheckoutAllowed; + + return this; + }, + + /** @inheritdoc */ + beforePayment: function (resolve, reject) { + var promise = $.Deferred(); + + if (this.declinePayment) { + this.addError(this.signInMessage, 'warning'); + + reject(); + } else { + promise.resolve(); } - }); - } -); + + return promise; + }, + + /** @inheritdoc */ + prepareClientConfig: function () { + this._super(); + this.clientConfig.commit = false; + + return this.clientConfig; + } + }); +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js new file mode 100644 index 0000000000000..ad7e86f2e99e0 --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js @@ -0,0 +1,123 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'underscore', + 'paypalInContextExpressCheckout' +], function (_, paypal) { + 'use strict'; + + /** + * Returns array of allowed funding + * + * @param {Object} config + * @return {Array} + */ + function getFunding(config) { + return _.map(config, function (name) { + return paypal.FUNDING[name]; + }); + } + + return function (clientConfig, element) { + paypal.Button.render({ + env: clientConfig.environment, + client: clientConfig.client, + locale: clientConfig.locale, + funding: { + allowed: getFunding(clientConfig.allowedFunding), + disallowed: getFunding(clientConfig.disallowedFunding) + }, + style: clientConfig.styles, + + // Enable Pay Now checkout flow (optional) + commit: clientConfig.commit, + + /** + * Validate payment method + * + * @param {Object} actions + */ + validate: function (actions) { + clientConfig.rendererComponent.validate(actions); + }, + + /** + * Execute logic on Paypal button click + */ + onClick: function () { + clientConfig.rendererComponent.onClick(); + }, + + /** + * Set up a payment + * + * @return {*} + */ + payment: function () { + var params = { + 'quote_id': clientConfig.quoteId, + 'customer_id': clientConfig.customerId || '', + 'form_key': clientConfig.formKey, + button: clientConfig.button + }; + + return new paypal.Promise(function (resolve, reject) { + clientConfig.rendererComponent.beforePayment(resolve, reject).then(function () { + paypal.request.post(clientConfig.getTokenUrl, params).then(function (res) { + return clientConfig.rendererComponent.afterPayment(res, resolve, reject); + }).catch(function (err) { + return clientConfig.rendererComponent.catchPayment(err, resolve, reject); + }); + }); + }); + }, + + /** + * Execute the payment + * + * @param {Object} data + * @param {Object} actions + * @return {*} + */ + onAuthorize: function (data, actions) { + var params = { + paymentToken: data.paymentToken, + payerId: data.payerID, + quoteId: clientConfig.quoteId || '', + customerId: clientConfig.customerId || '', + 'form_key': clientConfig.formKey + }; + + return new paypal.Promise(function (resolve, reject) { + clientConfig.rendererComponent.beforeOnAuthorize(resolve, reject, actions).then(function () { + paypal.request.post(clientConfig.onAuthorizeUrl, params).then(function (res) { + clientConfig.rendererComponent.afterOnAuthorize(res, resolve, reject, actions); + }).catch(function (err) { + return clientConfig.rendererComponent.catchOnAuthorize(err, resolve, reject); + }); + }); + }); + + }, + + /** + * Process cancel action + * + * @param {Object} data + * @param {Object} actions + */ + onCancel: function (data, actions) { + clientConfig.rendererComponent.onCancel(data, actions); + }, + + /** + * Process errors + */ + onError: function (err) { + clientConfig.rendererComponent.onError(err); + } + }, element); + }; +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js new file mode 100644 index 0000000000000..905f860fe2651 --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js @@ -0,0 +1,187 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate', + 'Magento_Customer/js/customer-data', + 'Magento_Paypal/js/in-context/express-checkout-smart-buttons', + 'mage/cookies' +], function ($, $t, customerData, checkoutSmartButtons) { + 'use strict'; + + return { + defaults: { + paymentActionError: $t('Something went wrong with your request. Please try again later.'), + signInMessage: $t('To check out, please sign in with your email address.') + }, + + /** + * Render PayPal buttons using checkout.js + */ + renderPayPalButtons: function (element) { + checkoutSmartButtons(this.prepareClientConfig(), element); + }, + + /** + * Validate payment method + * + * @param {Object} actions + */ + validate: function (actions) { + this.actions = actions || this.actions; + }, + + /** + * Execute logic on Paypal button click + */ + onClick: function () {}, + + /** + * Before payment execute + * + * @param {Function} resolve + * @param {Function} reject + * @return {*} + */ + beforePayment: function (resolve, reject) { //eslint-disable-line no-unused-vars + return $.Deferred().resolve(); + }, + + /** + * After payment execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * + * @return {*} + */ + afterPayment: function (res, resolve, reject) { + if (res.success) { + return resolve(res.token); + } + + this.addError(res['error_message']); + + return reject(new Error(res['error_message'])); + }, + + /** + * Catch payment + * + * @param {Error} err + * @param {Function} resolve + * @param {Function} reject + */ + catchPayment: function (err, resolve, reject) { + this.addError(this.paymentActionError); + reject(err); + }, + + /** + * Before onAuthorize execute + * + * @param {Function} resolve + * @param {Function} reject + * @param {Object} actions + * + * @return {jQuery.Deferred} + */ + beforeOnAuthorize: function (resolve, reject, actions) { //eslint-disable-line no-unused-vars + return $.Deferred().resolve(); + }, + + /** + * After onAuthorize execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * @param {Object} actions + * + * @return {*} + */ + afterOnAuthorize: function (res, resolve, reject, actions) { + if (res.success) { + resolve(); + + return actions.redirect(window, res.redirectUrl); + } + + this.addError(res['error_message']); + + return reject(new Error(res['error_message'])); + }, + + /** + * Catch payment + * + * @param {Error} err + * @param {Function} resolve + * @param {Function} reject + */ + catchOnAuthorize: function (err, resolve, reject) { + this.addError(this.paymentActionError); + reject(err); + }, + + /** + * Process cancel action + * + * @param {Object} data + * @param {Object} actions + */ + onCancel: function (data, actions) { + actions.redirect(window, this.clientConfig.onCancelUrl); + }, + + /** + * Process errors + * + * @param {Error} err + */ + onError: function (err) { //eslint-disable-line no-unused-vars + // Uncaught error isn't displayed in the console + }, + + /** + * Adds error message + * + * @param {String} message + * @param {String} [type] + */ + addError: function (message, type) { + type = type || 'error'; + customerData.set('messages', { + messages: [{ + type: type, + text: message + }], + 'data_id': Math.floor(Date.now() / 1000) + }); + }, + + /** + * @returns {String} + */ + getButtonId: function () { + return this.inContextId; + }, + + /** + * Populate client config with all required data + * + * @return {Object} + */ + prepareClientConfig: function () { + this.clientConfig.client = {}; + this.clientConfig.client[this.clientConfig.environment] = this.clientConfig.merchantId; + this.clientConfig.rendererComponent = this; + this.clientConfig.formKey = $.mage.cookies.get('form_key'); + + return this.clientConfig; + } + }; +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js new file mode 100644 index 0000000000000..413820cc731ac --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js @@ -0,0 +1,76 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'underscore', + 'jquery', + 'uiComponent', + 'Magento_Paypal/js/in-context/express-checkout-wrapper', + 'Magento_Customer/js/customer-data' +], function (_, $, Component, Wrapper, customerData) { + 'use strict'; + + return Component.extend(Wrapper).extend({ + defaults: { + productFormSelector: '#product_addtocart_form', + declinePayment: false, + formInvalid: false + }, + + /** @inheritdoc */ + initialize: function (config, element) { + var cart = customerData.get('cart'), + customer = customerData.get('customer'); + + this._super(); + this.renderPayPalButtons(element); + this.declinePayment = !customer().firstname && !cart().isGuestCheckoutAllowed; + + return this; + }, + + /** @inheritdoc */ + onClick: function () { + var $form = $(this.productFormSelector); + + if (!this.declinePayment) { + $form.submit(); + this.formInvalid = !$form.validation('isValid'); + } + }, + + /** @inheritdoc */ + beforePayment: function (resolve, reject) { + var promise = $.Deferred(); + + if (this.declinePayment) { + this.addError(this.signInMessage, 'warning'); + reject(); + } else if (this.formInvalid) { + reject(); + } else { + $(document).on('ajax:addToCart', function (e, data) { + if (_.isEmpty(data.response)) { + return promise.resolve(); + } + + return reject(); + }); + $(document).on('ajax:addToCart:error', reject); + } + + return promise; + }, + + /** @inheritdoc */ + prepareClientConfig: function () { + this._super(); + this.clientConfig.quoteId = ''; + this.clientConfig.customerId = ''; + this.clientConfig.commit = false; + + return this.clientConfig; + } + }); +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js index 3315a7c402d65..7fb94a7e2348e 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js @@ -4,10 +4,9 @@ */ define([ 'Magento_Checkout/js/view/payment/default', - 'ko', 'Magento_Paypal/js/model/iframe', 'Magento_Checkout/js/model/full-screen-loader' -], function (Component, ko, iframe, fullScreenLoader) { +], function (Component, iframe, fullScreenLoader) { 'use strict'; return Component.extend({ diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js index c56f21bc718fb..5c509238fe5cc 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js @@ -2,134 +2,103 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -define( - [ - 'underscore', - 'jquery', - 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-abstract', - 'Magento_Paypal/js/action/set-payment-method', - 'Magento_Checkout/js/model/payment/additional-validators', - 'Magento_Ui/js/lib/view/utils/dom-observer', - 'paypalInContextExpressCheckout', - 'Magento_Customer/js/customer-data', - 'Magento_Ui/js/model/messageList' - ], - function ( - _, - $, - Component, - setPaymentMethodAction, - additionalValidators, - domObserver, - paypalExpressCheckout, - customerData, - messageList - ) { - 'use strict'; - - // State of PayPal module initialization - var clientInit = false; - - return Component.extend({ - - defaults: { - template: 'Magento_Paypal/payment/paypal-express-in-context', - clientConfig: { - /** - * @param {Object} event - */ - click: function (event) { - event.preventDefault(); - - if (additionalValidators.validate()) { - paypalExpressCheckout.checkout.initXO(); - - this.selectPaymentMethod(); - setPaymentMethodAction(this.messageContainer).done(function () { - $('body').trigger('processStart'); - - $.getJSON(this.path, { - button: 0 - }).done(function (response) { - var message = response && response.message; - - if (message) { - if (message.type === 'error') { - messageList.addErrorMessage({ - message: message.text - }); - } else { - messageList.addSuccessMessage({ - message: message.text - }); - } - } - - if (response && response.url) { - paypalExpressCheckout.checkout.startFlow(response.url); - - return; - } - - paypalExpressCheckout.checkout.closeFlow(); - }).fail(function () { - paypalExpressCheckout.checkout.closeFlow(); - }).always(function () { - $('body').trigger('processStop'); - customerData.invalidate(['cart']); - }); - }.bind(this)).fail(function () { - paypalExpressCheckout.checkout.closeFlow(); - }); - } - } - } - }, - - /** - * @returns {Object} - */ - initialize: function () { - this._super(); - this.initClient(); - - return this; - }, +define([ + 'jquery', + 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-abstract', + 'Magento_Paypal/js/in-context/express-checkout-wrapper', + 'Magento_Paypal/js/action/set-payment-method', + 'Magento_Checkout/js/model/payment/additional-validators', + 'Magento_Ui/js/model/messageList', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, Component, Wrapper, setPaymentMethod, additionalValidators, messageList) { + 'use strict'; + + return Component.extend(Wrapper).extend({ + defaults: { + template: 'Magento_Paypal/payment/paypal-express-in-context', + validationElements: 'input' + }, + + /** + * Listens element on change and validate it. + * + * @param {HTMLElement} context + */ + initListeners: function (context) { + $.async(this.validationElements, context, function (element) { + $(element).on('change', function () { + this.validate(); + }.bind(this)); + }.bind(this)); + }, + + /** + * Validates Smart Buttons + */ + validate: function () { + this._super(); + + if (this.actions) { + additionalValidators.validate(true) ? this.actions.enable() : this.actions.disable(); + } + }, - /** - * @returns {Object} - */ - initClient: function () { - var selector = '#' + this.getButtonId(); + /** @inheritdoc */ + beforePayment: function (resolve, reject) { + var promise = $.Deferred(); - _.each(this.clientConfig, function (fn, name) { - if (typeof fn === 'function') { - this.clientConfig[name] = fn.bind(this); - } - }, this); + setPaymentMethod(this.messageContainer).done(function () { + return promise.resolve(); + }).fail(function (response) { + var error; - if (!clientInit) { - domObserver.get(selector, function () { - paypalExpressCheckout.checkout.setup(this.merchantId, this.clientConfig); - clientInit = true; - domObserver.off(selector); - }.bind(this)); - } else { - domObserver.get(selector, function () { - $(selector).on('click', this.clientConfig.click); - domObserver.off(selector); - }.bind(this)); + try { + error = JSON.parse(response.responseText); + } catch (exception) { + error = this.paymentActionError; } - return this; - }, - - /** - * @returns {String} - */ - getButtonId: function () { - return this.inContextId; - } - }); - } -); + this.addError(error); + + return reject(new Error(error)); + }.bind(this)); + + return promise; + }, + + /** + * Populate client config with all required data + * + * @return {Object} + */ + prepareClientConfig: function () { + this._super(); + this.clientConfig.quoteId = window.checkoutConfig.quoteData['entity_id']; + this.clientConfig.customerId = window.customerData.id; + this.clientConfig.merchantId = this.merchantId; + this.clientConfig.button = 0; + this.clientConfig.commit = true; + + return this.clientConfig; + }, + + /** + * Adding logic to be triggered onClick action for smart buttons component + */ + onClick: function () { + additionalValidators.validate(); + this.selectPaymentMethod(); + }, + + /** + * Adds error message + * + * @param {String} message + */ + addError: function (message) { + messageList.addErrorMessage({ + message: message + }); + } + }); +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js index d0d72bf7dcdf3..a0f3d3867fe78 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js @@ -2,12 +2,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/*browser:true*/ -/*global define*/ + define([ - 'jquery', 'Magento_Vault/js/view/payment/method-renderer/vault' -], function ($, VaultComponent) { +], function (VaultComponent) { 'use strict'; return VaultComponent.extend({ diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js index d038f08c348ec..b01d0454b55d9 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js @@ -15,7 +15,7 @@ define([ return Component.extend({ defaults: { - template: 'Magento_Paypal/payment/paypal-express-bml', + template: 'Magento_Paypal/payment/payflow-express-bml', billingAgreement: '' }, diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-bml.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-bml.js deleted file mode 100644 index 561b3c0e97168..0000000000000 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-bml.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-abstract' -], function (Component) { - 'use strict'; - - return Component.extend({ - defaults: { - template: 'Magento_Paypal/payment/paypal-express-bml' - } - }); -}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js index 9eae0dfb45e43..1628bbed8f1c6 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js @@ -19,10 +19,6 @@ define([ component: paypalExpress, config: window.checkoutConfig.payment.paypalExpress.inContextConfig }, - { - type: 'paypal_express_bml', - component: 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-bml' - }, { type: 'payflow_express', component: 'Magento_Paypal/js/view/payment/method-renderer/payflow-express' diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js b/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js index e181faf56e365..09dffc73baadf 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js @@ -8,9 +8,8 @@ */ define([ 'uiComponent', - 'ko', 'Magento_Paypal/js/model/iframe' -], function (Component, ko, iframe) { +], function (Component, iframe) { 'use strict'; return Component.extend({ diff --git a/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html index f1b14831bab31..d6fb2f3e6fc75 100644 --- a/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html +++ b/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html @@ -58,6 +58,7 @@ </div> <!-- /ko --> </form> + <div class="checkout-agreements-block"> <!-- ko foreach: $parent.getRegion('before-place-order') --> <!-- ko template: getTemplate() --><!-- /ko --> diff --git a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-bml.html b/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-bml.html deleted file mode 100644 index 0f042824fe898..0000000000000 --- a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-bml.html +++ /dev/null @@ -1,52 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> - <div class="payment-method-title field choice"> - <input type="radio" - name="payment[method]" - class="radio" - data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" /> - <label data-bind="attr: {'for': getCode()}" class="label"> - <!-- PayPal Logo --> - <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/ppc-acceptance-medium.png" - data-bind="attr: {alt: $t('Acceptance Mark')}" - class="payment-icon"/> - <!-- PayPal Logo --> - <span data-bind="text: getTitle()"></span> - <a href="https://www.securecheckout.billmelater.com/paycapture-content/fetch?hash=AU826TU8&content=/bmlweb/ppwpsiw.html" - data-bind="click: showAcceptanceWindow" - class="action action-help"> - <!-- ko i18n: 'See terms' --><!-- /ko --> - </a> - </label> - </div> - <div class="payment-method-content"> - <!-- ko foreach: getRegion('messages') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - <fieldset class="fieldset" data-bind='attr: {id: "payment_form_" + getCode()}'> - <div class="payment-method-note"> - <!-- ko i18n: 'You will be redirected to the PayPal website when you place an order.' --><!-- /ko --> - </div> - </fieldset> - <div class="checkout-agreements-block"> - <!-- ko foreach: $parent.getRegion('before-place-order') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - </div> - <div class="actions-toolbar"> - <div class="primary"> - <button class="action primary checkout" - type="submit" - data-bind="click: continueToPayPal, enable: (getCode() == isChecked())" - disabled> - <span data-bind="i18n: 'Continue to PayPal'"></span> - </button> - </div> - </div> - </div> -</div> diff --git a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html b/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html index 562243decaa6b..5f32183252341 100644 --- a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html +++ b/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html @@ -4,41 +4,32 @@ * See COPYING.txt for license details. */ --> -<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> +<div class="payment-method" css="_active: getCode() == isChecked()" afterRender="initListeners"> <div class="payment-method-title field choice"> <input type="radio" name="payment[method]" class="radio" - data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" /> - <label data-bind="attr: {'for': getCode()}" class="label"> + attr="id: getCode()" + ko-value="getCode()" + ko-checked="isChecked" + click="selectPaymentMethod" + visible="isRadioButtonVisible()"/> + <label attr="for: getCode()" class="label"> <!-- PayPal Logo --> - <img data-bind="attr: {src: getPaymentAcceptanceMarkSrc(), alt: $t('Acceptance Mark')}" class="payment-icon"/> + <img attr="src: getPaymentAcceptanceMarkSrc(), alt: $t('Acceptance Mark')" class="payment-icon"/> <!-- PayPal Logo --> - <span data-bind="text: getTitle()"></span> - <a data-bind="attr: {href: getPaymentAcceptanceMarkHref()}, click: showAcceptanceWindow" - class="action action-help"> - <!-- ko i18n: 'What is PayPal?' --><!-- /ko --> - </a> + <span text="getTitle()"/> + <a class="action action-help" + attr="href: getPaymentAcceptanceMarkHref()" + click="showAcceptanceWindow" + translate="'What is PayPal?'"/> </label> </div> <div class="payment-method-content"> - <!-- ko foreach: getRegion('messages') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> + <each args="getRegion('messages')" render=""/> <div class="checkout-agreements-block"> - <!-- ko foreach: $parent.getRegion('before-place-order') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - </div> - <div class="actions-toolbar"> - <div class="primary"> - <button class="action primary checkout" - type="submit" - data-bind="enable: (getCode() == isChecked()), attr: {id: getButtonId()}" - disabled> - <span data-bind="i18n: 'Continue to PayPal'"></span> - </button> - </div> + <each args="$parent.getRegion('before-place-order')" render=""/> </div> + <div class="actions-toolbar" attr="id: getButtonId()" afterRender="renderPayPalButtons"/> </div> </div> diff --git a/app/code/Magento/PaypalCaptcha/LICENSE.txt b/app/code/Magento/PaypalCaptcha/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php new file mode 100644 index 0000000000000..289a1631ed1f6 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalCaptcha\Model\Checkout; + +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\CaptchaInterface; +use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Configuration provider for Captcha rendering. + */ +class ConfigProviderPayPal implements ConfigProviderInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Data + */ + private $captchaData; + + /** + * @var string + */ + private static $formId = 'co-payment-form'; + + /** + * @param StoreManagerInterface $storeManager + * @param Data $captchaData + */ + public function __construct( + StoreManagerInterface $storeManager, + Data $captchaData + ) { + $this->storeManager = $storeManager; + $this->captchaData = $captchaData; + } + + /** + * @inheritdoc + */ + public function getConfig(): array + { + $config['captchaPayments'][self::$formId] = [ + 'isCaseSensitive' => $this->isCaseSensitive(self::$formId), + 'imageHeight' => $this->getImageHeight(self::$formId), + 'imageSrc' => $this->getImageSrc(self::$formId), + 'refreshUrl' => $this->getRefreshUrl(), + 'isRequired' => $this->isRequired(self::$formId), + 'timestamp' => time() + ]; + + return $config; + } + + /** + * Returns is captcha case sensitive + * + * @param string $formId + * @return bool + */ + private function isCaseSensitive(string $formId): bool + { + return (bool)$this->getCaptchaModel($formId)->isCaseSensitive(); + } + + /** + * Returns captcha image height + * + * @param string $formId + * @return int + */ + private function getImageHeight(string $formId): int + { + return (int)$this->getCaptchaModel($formId)->getHeight(); + } + + /** + * Returns captcha image source path + * + * @param string $formId + * @return string + */ + private function getImageSrc(string $formId): string + { + if ($this->isRequired($formId)) { + $captcha = $this->getCaptchaModel($formId); + $captcha->generate(); + return $captcha->getImgSrc(); + } + + return ''; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + private function getRefreshUrl(): string + { + $store = $this->storeManager->getStore(); + return $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]); + } + + /** + * Whether captcha is required to be inserted to this form + * + * @param string $formId + * @return bool + */ + private function isRequired(string $formId): bool + { + return (bool)$this->getCaptchaModel($formId)->isRequired(); + } + + /** + * Return captcha model for specified form + * + * @param string $formId + * @return CaptchaInterface + */ + private function getCaptchaModel(string $formId): CaptchaInterface + { + return $this->captchaData->getCaptcha($formId); + } +} diff --git a/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php new file mode 100644 index 0000000000000..e7cb282b1799b --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalCaptcha\Observer; + +use Magento\Captcha\Helper\Data; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\ActionFlag ; + +/** + * Validates Captcha for Request Token controller + */ +class CaptchaRequestToken implements ObserverInterface +{ + /** + * @var Data + */ + private $helper; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var ActionFlag + */ + private $actionFlag; + + /** + * @param Data $helper + * @param Json $jsonSerializer + * @param ActionFlag $actionFlag + */ + public function __construct(Data $helper, Json $jsonSerializer, ActionFlag $actionFlag) + { + $this->helper = $helper; + $this->jsonSerializer = $jsonSerializer; + $this->actionFlag = $actionFlag; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + $formId = 'co-payment-form'; + $captcha = $this->helper->getCaptcha($formId); + + if (!$captcha->isRequired()) { + return; + } + + /** @var Action $controller */ + $controller = $observer->getControllerAction(); + $word = $controller->getRequest()->getPost('captcha_string'); + if ($captcha->isCorrect($word)) { + return; + } + + $data = $this->jsonSerializer->serialize([ + 'success' => false, + 'error' => true, + 'error_messages' => __('Incorrect CAPTCHA.') + ]); + $this->actionFlag->set('', Action::FLAG_NO_DISPATCH, true); + $controller->getResponse()->representJson($data); + } +} diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md new file mode 100644 index 0000000000000..71588599a5ecd --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/README.md @@ -0,0 +1 @@ +The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. \ No newline at end of file diff --git a/app/code/Magento/PaypalCaptcha/composer.json b/app/code/Magento/PaypalCaptcha/composer.json new file mode 100644 index 0000000000000..e71ef8c0ec7de --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-paypal-captcha", + "description": "Provides CAPTCHA validation for PayPal Payflow Pro", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-captcha": "*", + "magento/module-checkout": "*", + "magento/module-store": "*" + }, + "suggest": { + "magento/module-paypal": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\PaypalCaptcha\\": "" + } + } +} diff --git a/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..12afd8ceda60e --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="customer"> + <group id="captcha" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="forms" translate="label comment" type="multiselect" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <comment>CAPTCHA for "Create user", "Forgot password", "Payflow Pro" forms is always enabled if chosen.</comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/config.xml b/app/code/Magento/PaypalCaptcha/etc/config.xml new file mode 100644 index 0000000000000..133a78a42f7b4 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/config.xml @@ -0,0 +1,30 @@ +<?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:module:Magento_Store:etc/config.xsd"> + <default> + <customer> + <captcha> + <shown_to_logged_in_user> + <co-payment-form>1</co-payment-form> + </shown_to_logged_in_user> + <always_for> + <co-payment-form>1</co-payment-form> + </always_for> + </captcha> + </customer> + <captcha translate="label"> + <frontend> + <areas> + <co-payment-form> + <label>Payflow Pro</label> + </co-payment-form> + </areas> + </frontend> + </captcha> + </default> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml new file mode 100644 index 0000000000000..c236d5ea04ca0 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml @@ -0,0 +1,16 @@ +<?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\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="paypal_captcha_config_provider" xsi:type="object">Magento\PaypalCaptcha\Model\Checkout\ConfigProviderPayPal</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml new file mode 100644 index 0000000000000..ae706c4485d61 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?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:Event/etc/events.xsd"> + <event name="controller_action_predispatch_paypal_transparent_requestsecuretoken"> + <observer name="captcha_request_token" instance="Magento\PaypalCaptcha\Observer\CaptchaRequestToken"/> + </event> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/module.xml b/app/code/Magento/PaypalCaptcha/etc/module.xml new file mode 100644 index 0000000000000..425c829a5d391 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/module.xml @@ -0,0 +1,15 @@ +<?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_PaypalCaptcha" > + <sequence> + <module name="Magento_Captcha"/> + <module name="Magento_Paypal"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/PaypalCaptcha/registration.php b/app/code/Magento/PaypalCaptcha/registration.php new file mode 100644 index 0000000000000..4dac0582a6d1b --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_PaypalCaptcha', __DIR__); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 0000000000000..9837068faab73 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout.root"> + <arguments> + <argument name="jsLayout" xsi:type="array"> + <item name="components" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="steps" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="billing-step" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="payment" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="payments-list" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="paypal-captcha" xsi:type="array"> + <item name="component" xsi:type="string">uiComponent</item> + <item name="displayArea" xsi:type="string">paypal-captcha</item> + <item name="dataScope" xsi:type="string">paypal-captcha</item> + <item name="provider" xsi:type="string">checkoutProvider</item> + <item name="config" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Checkout/payment/before-place-order</item> + </item> + <item name="children" xsi:type="array"> + <item name="captcha" xsi:type="array"> + <item name="component" xsi:type="string">Magento_PaypalCaptcha/js/view/checkout/paymentCaptcha</item> + <item name="displayArea" xsi:type="string">paypal-captcha</item> + <item name="formId" xsi:type="string">co-payment-form</item> + <item name="configSource" xsi:type="string">checkoutConfig</item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..78e7add4ec690 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Checkout/js/view/payment/list': { + 'Magento_PaypalCaptcha/js/view/payment/list-mixin': true + } + } + } +}; diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js new file mode 100644 index 0000000000000..f8f119e3b3396 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js @@ -0,0 +1,44 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Captcha/js/model/captcha' +], +function ($, defaultCaptcha, captchaList, Captcha) { + 'use strict'; + + return defaultCaptcha.extend({ + + /** @inheritdoc */ + initialize: function () { + var captchaConfigPayment, + currentCaptcha; + + this._super(); + + if (window[this.configSource] && window[this.configSource].captchaPayments) { + captchaConfigPayment = window[this.configSource].captchaPayments; + + $.each(captchaConfigPayment, function (formId, captchaData) { + var captcha; + + captchaData.formId = formId; + captcha = Captcha(captchaData); + captchaList.add(captcha); + }); + } + + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + } + } + }); +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js new file mode 100644 index 0000000000000..60172f696e9ed --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js @@ -0,0 +1,54 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/model/captchaList' +], function ($, captchaList) { + 'use strict'; + + var mixin = { + + formId: 'co-payment-form', + + /** + * Sets custom template for Payflow Pro + * + * @param {Object} payment + * @returns {Object} + */ + createComponent: function (payment) { + + var component = this._super(payment); + + if (component.component === 'Magento_Paypal/js/view/payment/method-renderer/payflowpro-method') { + component.template = 'Magento_PaypalCaptcha/payment/payflowpro-form'; + $(window).off('clearTimeout') + .on('clearTimeout', this.clearTimeout.bind(this)); + } + + return component; + }, + + /** + * Overrides default window.clearTimeout() to catch errors from iframe and reload Captcha. + */ + clearTimeout: function () { + var captcha = captchaList.getCaptchaByFormId(this.formId); + + if (captcha !== null) { + captcha.refresh(); + } + clearTimeout(); + } + }; + + /** + * Overrides `Magento_Checkout/js/view/payment/list::createComponent` + */ + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html new file mode 100644 index 0000000000000..fec5cf96b0324 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html @@ -0,0 +1,90 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> + <div class="payment-method-title field choice"> + <input type="radio" + name="payment[method]" + class="radio" + data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/> + <label class="label" data-bind="attr: {'for': getCode()}"> + <span data-bind="text: getTitle()"></span> + </label> + </div> + + <div class="payment-method-content"> + <!-- ko foreach: getRegion('messages') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + <div class="payment-method-billing-address"> + <!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + </div> + <iframe width="0" + height="0" + data-bind="src: getSource(), attr: {id: getCode() + '-transparent-iframe', 'data-container': getCode() + '-transparent-iframe'}" + allowtransparency="true" + frameborder="0" + name="iframeTransparent" + class="payment-method-iframe"> + </iframe> + <form class="form" id="co-transparent-form" action="#" method="post" data-bind="mageInit: { + 'transparent':{ + 'context': context(), + 'controller': getControllerName(), + 'gateway': getCode(), + 'orderSaveUrl':getPlaceOrderUrl(), + 'cgiUrl': getCgiUrl(), + 'dateDelim': getDateDelim(), + 'cardFieldsMap': getCardFieldsMap(), + 'nativeAction': getSaveOrderUrl() + }, 'validation':[]}"> + + <!-- ko template: 'Magento_Payment/payment/cc-form' --><!-- /ko --> + + <!-- ko if: (isVaultEnabled())--> + <div class="field-tooltip-content"> + <input type="checkbox" + name="vault[is_enabled]" + class="checkbox-inline" + data-bind="attr: {'id': getCode() + '_enable_vault'}, checked: vaultEnabler.isActivePaymentTokenEnabler"/> + <label class="label" data-bind="attr: {'for': getCode() + '_enable_vault'}"> + <span><!-- ko i18n: 'Save credit card information for future use.'--><!-- /ko --></span> + </label> + </div> + <!-- /ko --> + </form> + <fieldset class="fieldset payment items ccard"> + <!-- ko foreach: $parent.getRegion('paypal-captcha') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!-- /ko --> + </fieldset> + + + <div class="checkout-agreements-block"> + <!-- ko foreach: $parent.getRegion('before-place-order') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + </div> + <div class="actions-toolbar"> + <div class="primary"> + <button data-role="review-save" + type="submit" + data-bind=" + attr: {title: $t('Place Order')}, + enable: (getCode() == isChecked()), + click: placeOrder, + css: {disabled: !isPlaceOrderActionAllowed()} + " + class="action primary checkout" + disabled> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index 35c2c70be30dc..8ae22e4c26c6f 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -7,6 +7,8 @@ /** * Class QuoteManager + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteManager { @@ -87,6 +89,7 @@ public function setGuest($checkQuote = false) ->setCustomerLastname(null) ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) + ->setCustomerIsGuest(true) ->removeAllAddresses(); //Create guest addresses $quote->getShippingAddress(); diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index f3720960ca6e5..79fdf44c3c551 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Observer of expired session + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckExpirePersistentQuoteObserver implements ObserverInterface { /** @@ -107,8 +111,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && - !$this->isRequestFromCheckoutPage($this->request) + !$this->isRequestFromCheckoutPage($this->request) && // persistent session does not expire on onepage checkout page + ( + $this->_checkoutSession->getQuote()->getIsPersistent() || + $this->_checkoutSession->getQuote()->getCustomerIsGuest() + ) ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); diff --git a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php index db6b6d1ee370d..2803bc998dcbe 100644 --- a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php +++ b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Observer for setting "is_persistent" value to quote + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class SetQuotePersistentDataObserver implements ObserverInterface { /** @@ -73,8 +77,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if (( - ($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - && !$this->_persistentData->isShoppingCartPersist() + ($this->_persistentSession->isPersistent()) + && $this->_persistentData->isShoppingCartPersist() ) && $this->quoteManager->isPersistent() ) { diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml new file mode 100644 index 0000000000000..e5c77ee414362 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml @@ -0,0 +1,84 @@ +<?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="ShippingQuotePersistedForGuestTest"> + <annotations> + <features value="Persistent"/> + <stories value="Guest checkout"/> + <title value="Estimate Shipping and Tax block sections on shipping cart saving correctly for Guest."/> + <description value="Verify that 'Estimate Shipping and Tax' block sections on shipping cart saving correctly for Guest after switching to another page. And check that the shopping cart is cleared after reset persistent cookie."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-99025"/> + <useCaseId value="MAGETWO-98620"/> + <group value="persistent"/> + </annotations> + <before> + <!--Enabled The Persistent Shopping Cart feature --> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> + <!--Create simple product--> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">150</field> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Revert persistent configuration to default--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Step 1: Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Step 2: Open the Product Page and add the product to shopping cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsLoggedUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsLoggedUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!--Step 3: Log out, reset persistent cookie and go to homepage--> + <amOnPage url="{{StorefrontCustomerSignOutPage.url}}" stepKey="signOut"/> + <waitForLoadingMaskToDisappear stepKey="waitSignOutPage"/> + <resetCookie userInput="persistent_shopping_cart" stepKey="resetPersistentCookie"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePageAfterResetPersistentCookie"/> + <waitForPageLoad stepKey="waitHomePageLoadAfterResetCookie"/> + <!--Check that the minicart is empty--> + <actionGroup ref="assertMiniCartEmpty" after="waitHomePageLoadAfterResetCookie" stepKey="seeMinicartEmpty"/> + <!--Step 4: Add the product to shopping cart and open cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsGuestUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsGuestUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartBeforeChangeShippingAndTaxSection"/> + <!--Step 5: Open Estimate Shipping and Tax block and fill the sections--> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTax" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="selectUSCountry"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="selectCaliforniaRegion"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="inputPostCode"/> + <!--Step 6: Go to Homepage--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> + <!--Step 7: Go to shopping cart and check "Estimate Shipping and Tax" fields values are saved--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" after="goToHomePageAfterChangingShippingAndTaxSection" stepKey="goToShoppingCartAfterChangingShippingAndTaxSection"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTaxAfterChanging" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml new file mode 100644 index 0000000000000..dc6f87bef0ba8 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml @@ -0,0 +1,193 @@ +<?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="StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Catalog widget"/> + <title value="Verify that information about viewing, comparison, wishlist and last ordered items is persisted under long-term cookie"/> + <description value="Verify that information about viewing, comparison, wishlist and last ordered items is persisted under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12180"/> + <group value="persistent"/> + <group value="widget"/> + <group value="catalog_widget"/> + <skip> + <issueId value="MC-15741"/> + </skip> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> + <createData entity="EnableSynchronizeWidgetProductsWithBackendStorage" stepKey="enableSynchronizeWidgetProductsWithBackendStorage"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyComparedProductsWidget"> + <argument name="widget" value="RecentlyComparedProductsWidget"/> + </actionGroup> + <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyViewedProductsWidget"> + <argument name="widget" value="RecentlyViewedProductsWidget"/> + </actionGroup> + </before> + <after> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + <createData entity="DisableSynchronizeWidgetProductsWithBackendStorage" stepKey="disableSynchronizeWidgetProductsWithBackendStorage"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromCustomer"/> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> + <argument name="widget" value="RecentlyComparedProductsWidget"/> + </actionGroup> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyViewedProductsWidget"> + <argument name="widget" value="RecentlyViewedProductsWidget"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessage"/> + + <!--Open the details page of Simple Product 1, Simple Product 2 and add to cart, get to the category--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSecondSimpleProductProductToCart"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterAddedProductToCart"/> + <!--The Recently Viewed widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="seeSimpleProductInRecentlyViewedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="seeSecondSimpleProductInRecentlyViewedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Add Simple Product 1 and Simple Product 2 to Wishlist--> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSimpleProductToWishlist"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterProductAddToWishlist"/> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSecondSimpleProductToWishlist"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <!--The My Wishlist widget displays Simple Product 1 and Simple Product 2--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckProductsInWishlistSidebar"/> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductInWishlistSidebar"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSecondSimpleProductInWishlistSidebar"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Add to compare Simple Product and Simple Product 2--> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSimpleProductToCompare" > + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSecondSimpleProductToCompare" > + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <!--The Compare Products widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSimpleProductInCompareSidebar"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSecondSimpleProductInCompareSidebar"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Click Clear all in the Compare Products widget--> + <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="clearCompareList"/> + <!--The Recently Compared widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSecondSimpleProductInRecentlyComparedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!--The Recently Ordered widget displays Simple Product 1 and Simple Product 2--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckProductsInRecentlyOrderedWidget"/> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSecondSimpleProductInRecentlyOrderedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Sign out and check that widgets persist the information about the items--> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="logoutFromCustomerToCheckThatWidgetsPersist"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterLogoutFromCustomer"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessageAfterLogoutFromCustomer"/> + <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYouAfterLogoutFromCustomer"/> + + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyViewedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductInWishlistSidebarAfterLogout"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Click the *Not you?* link and check the price for Simple Product--> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickLinkNotYou"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClickLinkNotYou"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterClickNotYou"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessageAfterClickLinkNotYou"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyViewedWidget"/> + <dontSee selector="{{StorefrontCustomerWishlistSidebarSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeProductInWishlistWidget"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyComparedWidget"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyOrderedWidget"/> + + <!--Login to storefront from customer again--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomerAfterClearLongTermCookie"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckWidgets"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessageAfterLogin"/> + + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductNameInWishlistSidebarAfterLogin"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyViewedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index 46dda1be365d4..b096dd2317a33 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Persistent\Test\Unit\Observer; +use Magento\Quote\Model\Quote; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -54,6 +58,11 @@ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase */ private $requestMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Quote + */ + private $quoteMock; + /** * @inheritdoc */ @@ -83,6 +92,10 @@ protected function setUp() $this->checkoutSessionMock, $this->requestMock ); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->setMethods(['getCustomerIsGuest', 'getIsPersistent']) + ->disableOriginalConstructor() + ->getMock(); } public function testExecuteWhenCanNotApplyPersistentData() @@ -133,6 +146,11 @@ public function testExecuteWhenPersistentIsEnabled( ->willReturn(true); $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(true); $this->sessionMock->expects($this->once())->method('isPersistent')->willReturn(false); + $this->checkoutSessionMock + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteMock->method('getCustomerIsGuest')->willReturn(true); + $this->quoteMock->method('getIsPersistent')->willReturn(true); $this->customerSessionMock ->expects($this->atLeastOnce()) ->method('isLoggedIn') diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php index 6724743789cea..ffa829e8456cc 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php @@ -7,6 +7,9 @@ namespace Magento\Persistent\Test\Unit\Observer; +/** + * Observer test for setting "is_persistent" value to quote + */ class SetQuotePersistentDataObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -83,7 +86,6 @@ public function testExecuteWhenQuoteNotExist() ->method('getEvent') ->will($this->returnValue($this->eventManagerMock)); $this->eventManagerMock->expects($this->once())->method('getQuote'); - $this->customerSessionMock->expects($this->never())->method('isLoggedIn'); $this->model->execute($this->observerMock); } @@ -98,8 +100,7 @@ public function testExecuteWhenSessionIsPersistent() ->expects($this->once()) ->method('getQuote') ->will($this->returnValue($this->quoteMock)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(false)); + $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true)); $this->quoteManagerMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); $this->quoteMock->expects($this->once())->method('setIsPersistent')->with(true); $this->model->execute($this->observerMock); diff --git a/app/code/Magento/ProductAlert/Model/Email.php b/app/code/Magento/ProductAlert/Model/Email.php index 7aee4ca01240d..3351166aa6a12 100644 --- a/app/code/Magento/ProductAlert/Model/Email.php +++ b/app/code/Magento/ProductAlert/Model/Email.php @@ -39,6 +39,8 @@ * * @api * @since 100.0.2 + * @method int getStoreId() + * @method $this setStoreId() */ class Email extends AbstractModel { @@ -136,11 +138,6 @@ class Email extends AbstractModel */ protected $_customerHelper; - /** - * @var int - */ - private $storeId = null; - /** * @param Context $context * @param Registry $registry @@ -215,18 +212,6 @@ public function setWebsite(\Magento\Store\Model\Website $website) return $this; } - /** - * Set store id from product alert. - * - * @param int $storeId - * @return $this - */ - public function setStoreId(int $storeId) - { - $this->storeId = $storeId; - return $this; - } - /** * Set website id * @@ -357,7 +342,7 @@ public function send() return false; } - $storeId = $this->storeId ?: (int) $this->_customer->getStoreId(); + $storeId = $this->getStoreId() ?: (int) $this->_customer->getStoreId(); $store = $this->getStore($storeId); $this->_appEmulation->startEnvironmentEmulation($storeId); diff --git a/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php b/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php index 710ede8ecefa6..c7b3d59138ecc 100644 --- a/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php @@ -5,6 +5,8 @@ */ namespace Magento\ProductAlert\Model\ResourceModel; +use Magento\Framework\Model\AbstractModel; + /** * Product alert for back in abstract resource model * @@ -15,13 +17,13 @@ abstract class AbstractResource extends \Magento\Framework\Model\ResourceModel\D /** * Retrieve alert row by object parameters * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return array|false */ - protected function _getAlertRow(\Magento\Framework\Model\AbstractModel $object) + protected function _getAlertRow(AbstractModel $object) { $connection = $this->getConnection(); - if ($object->getCustomerId() && $object->getProductId() && $object->getWebsiteId()) { + if ($this->isExistAllBindIds($object)) { $select = $connection->select()->from( $this->getMainTable() )->where( @@ -30,24 +32,41 @@ protected function _getAlertRow(\Magento\Framework\Model\AbstractModel $object) 'product_id = :product_id' )->where( 'website_id = :website_id' + )->where( + 'store_id = :store_id' ); $bind = [ ':customer_id' => $object->getCustomerId(), ':product_id' => $object->getProductId(), ':website_id' => $object->getWebsiteId(), + ':store_id' => $object->getStoreId() ]; return $connection->fetchRow($select, $bind); } return false; } + /** + * Is exists all bind ids. + * + * @param AbstractModel $object + * @return bool + */ + private function isExistAllBindIds(AbstractModel $object): bool + { + return ($object->getCustomerId() + && $object->getProductId() + && $object->getWebsiteId() + && $object->getStoreId()); + } + /** * Load object data by parameters * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return $this */ - public function loadByParam(\Magento\Framework\Model\AbstractModel $object) + public function loadByParam(AbstractModel $object) { $row = $this->_getAlertRow($object); if ($row) { @@ -59,13 +78,13 @@ public function loadByParam(\Magento\Framework\Model\AbstractModel $object) /** * Delete all customer alerts on website * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @param int $customerId * @param int $websiteId * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function deleteCustomer(\Magento\Framework\Model\AbstractModel $object, $customerId, $websiteId = null) + public function deleteCustomer(AbstractModel $object, $customerId, $websiteId = null) { $connection = $this->getConnection(); $where = []; diff --git a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php index ce1493b349a85..42407ca6be0b8 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php @@ -19,6 +19,8 @@ class CreateHandler extends AbstractHandler const ADDITIONAL_STORE_DATA_KEY = 'additional_store_data'; /** + * Execute before Plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @param array $arguments @@ -44,6 +46,8 @@ public function beforeExecute( } /** + * Execute plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Model\Product @@ -58,6 +62,9 @@ public function afterExecute( ); if (!empty($mediaCollection)) { + if ($product->getIsDuplicate() === true) { + $mediaCollection = $this->makeAllNewVideos($product->getId(), $mediaCollection); + } $newVideoCollection = $this->collectNewVideos($mediaCollection); $this->saveVideoData($newVideoCollection, 0); @@ -70,6 +77,8 @@ public function afterExecute( } /** + * Saves video data + * * @param array $videoDataCollection * @param int $storeId * @return void @@ -83,6 +92,8 @@ protected function saveVideoData(array $videoDataCollection, $storeId) } /** + * Saves additioanal video data + * * @param array $videoDataCollection * @return void */ @@ -99,6 +110,8 @@ protected function saveAdditionalStoreData(array $videoDataCollection) } /** + * Saves video data + * * @param array $item * @return void */ @@ -111,6 +124,8 @@ protected function saveVideoValuesItem(array $item) } /** + * Excludes current store data + * * @param array $mediaCollection * @param int $currentStoreId * @return array @@ -126,6 +141,8 @@ function ($item) use ($currentStoreId) { } /** + * Prepare video data for saving + * * @param array $rowData * @return array */ @@ -143,6 +160,8 @@ protected function prepareVideoRowDataForSave(array $rowData) } /** + * Loads video data + * * @param array $mediaCollection * @param int $excludedStore * @return array @@ -165,6 +184,8 @@ protected function loadStoreViewVideoData(array $mediaCollection, $excludedStore } /** + * Collect video data + * * @param array $mediaCollection * @return array */ @@ -182,6 +203,8 @@ protected function collectVideoData(array $mediaCollection) } /** + * Extract video data + * * @param array $rowData * @return array */ @@ -194,6 +217,8 @@ protected function extractVideoDataFromRowData(array $rowData) } /** + * Collect items for additional data adding + * * @param array $mediaCollection * @return array */ @@ -209,6 +234,8 @@ protected function collectVideoEntriesIdsToAdditionalLoad(array $mediaCollection } /** + * Add additional data + * * @param array $mediaCollection * @param array $data * @return array @@ -229,6 +256,8 @@ protected function addAdditionalStoreData(array $mediaCollection, array $data): } /** + * Creates additional video data + * * @param array $storeData * @param int $valueId * @return array @@ -247,6 +276,8 @@ protected function createAdditionalStoreDataCollection(array $storeData, $valueI } /** + * Collect new videos + * * @param array $mediaCollection * @return array */ @@ -262,6 +293,8 @@ private function collectNewVideos(array $mediaCollection): array } /** + * Checks if gallery item is video + * * @param array $item * @return bool */ @@ -273,6 +306,8 @@ private function isVideoItem(array $item): bool } /** + * Checks if video is new + * * @param array $item * @return bool */ @@ -282,4 +317,23 @@ private function isNewVideo(array $item): bool || empty($item['video_url_default']) || empty($item['video_title_default']); } + + /** + * Mark all videos as new + * + * @param int $entityId + * @param array $mediaCollection + * @return array + */ + private function makeAllNewVideos($entityId, array $mediaCollection): array + { + foreach ($mediaCollection as $key => $video) { + if ($this->isVideoItem($video)) { + unset($video['video_url_default'], $video['video_title_default']); + $video['entity_id'] = $entityId; + $mediaCollection[$key] = $video; + } + } + return $mediaCollection; + } } diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml index bd7cc0cdf5b4a..2b5f87f78d5e5 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoSimpleProductTest"> <annotations> <group value="ProductVideo"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml index f5a7886fed45c..d4da0ffa54451 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <group value="ProductVideo"/> diff --git a/app/code/Magento/ProductVideo/i18n/de_DE.csv b/app/code/Magento/ProductVideo/i18n/de_DE.csv deleted file mode 100644 index ca24668bb8d16..0000000000000 --- a/app/code/Magento/ProductVideo/i18n/de_DE.csv +++ /dev/null @@ -1,10 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" -"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/es_ES.csv b/app/code/Magento/ProductVideo/i18n/es_ES.csv deleted file mode 100644 index ca24668bb8d16..0000000000000 --- a/app/code/Magento/ProductVideo/i18n/es_ES.csv +++ /dev/null @@ -1,10 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" -"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/fr_FR.csv b/app/code/Magento/ProductVideo/i18n/fr_FR.csv deleted file mode 100644 index ca24668bb8d16..0000000000000 --- a/app/code/Magento/ProductVideo/i18n/fr_FR.csv +++ /dev/null @@ -1,10 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" -"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/nl_NL.csv b/app/code/Magento/ProductVideo/i18n/nl_NL.csv deleted file mode 100644 index 5ad8386573040..0000000000000 --- a/app/code/Magento/ProductVideo/i18n/nl_NL.csv +++ /dev/null @@ -1,10 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" -"Delete image in all store views","Delete image in all store views" \ No newline at end of file diff --git a/app/code/Magento/ProductVideo/i18n/pt_BR.csv b/app/code/Magento/ProductVideo/i18n/pt_BR.csv deleted file mode 100644 index 5ad8386573040..0000000000000 --- a/app/code/Magento/ProductVideo/i18n/pt_BR.csv +++ /dev/null @@ -1,10 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" -"Delete image in all store views","Delete image in all store views" \ No newline at end of file diff --git a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv b/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv deleted file mode 100644 index 5ad8386573040..0000000000000 --- a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,10 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" -"Delete image in all store views","Delete image in all store views" \ No newline at end of file diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index 20deba5b9b46a..cd0f3b3d630a6 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -177,12 +177,14 @@ define([ * @private */ clearEvents: function () { - this.fotoramaItem.off( - 'fotorama:show.' + this.PV + - ' fotorama:showend.' + this.PV + - ' fotorama:fullscreenenter.' + this.PV + - ' fotorama:fullscreenexit.' + this.PV - ); + if (this.fotoramaItem !== undefined) { + this.fotoramaItem.off( + 'fotorama:show.' + this.PV + + ' fotorama:showend.' + this.PV + + ' fotorama:fullscreenenter.' + this.PV + + ' fotorama:fullscreenexit.' + this.PV + ); + } }, /** diff --git a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php index e18ab8587fc71..60e5ad9f4caff 100644 --- a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php +++ b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc * * @param int $cartId The cart ID. * @return Totals Quote totals data. diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 3f04519713687..b1f68d0411cf0 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -1375,14 +1375,13 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add * * @param bool $useCache * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getItemsCollection($useCache = true) { - if ($this->hasItemsCollection()) { + if ($this->hasItemsCollection() && $useCache) { return $this->getData('items_collection'); } - if (null === $this->_items) { + if (null === $this->_items || !$useCache) { $this->_items = $this->_quoteItemCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($this->_items); $this->_items->setQuote($this); @@ -1399,7 +1398,7 @@ public function getAllItems() { $items = []; foreach ($this->getItemsCollection() as $item) { - /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $item */ + /** @var \Magento\Quote\Model\Quote\Item $item */ if (!$item->isDeleted()) { $items[] = $item; } @@ -2246,6 +2245,11 @@ public function validateMinimumAmount($multishipping = false) if (!$minOrderActive) { return true; } + $includeDiscount = $this->_scopeConfig->getValue( + 'sales/minimum_order/include_discount_amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); $minOrderMulti = $this->_scopeConfig->isSetFlag( 'sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -2279,7 +2283,10 @@ public function validateMinimumAmount($multishipping = false) $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; foreach ($address->getQuote()->getItemsCollection() as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ - $amount = $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes; + $amount = $includeDiscount ? + $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes : + $item->getBaseRowTotal() + $taxes; + if ($amount < $minAmount) { return false; } @@ -2289,7 +2296,9 @@ public function validateMinimumAmount($multishipping = false) $baseTotal = 0; foreach ($addresses as $address) { $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; - $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes; + $baseTotal += $includeDiscount ? + $address->getBaseSubtotalWithDiscount() + $taxes : + $address->getBaseSubtotal() + $taxes; } if ($baseTotal < $minAmount) { return false; diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index bafd6634a94c3..3ecbc69b80785 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -1149,6 +1149,11 @@ public function validateMinimumAmount() return true; } + $includeDiscount = $this->_scopeConfig->getValue( + 'sales/minimum_order/include_discount_amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); $amount = $this->_scopeConfig->getValue( 'sales/minimum_order/amount', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -1159,9 +1164,12 @@ public function validateMinimumAmount() \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $storeId ); + $taxes = $taxInclude ? $this->getBaseTaxAmount() : 0; - return ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount); + return $includeDiscount ? + ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount) : + ($this->getBaseSubtotal() + $taxes >= $amount); } /** diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index c5b8dc1c4b124..6fdb70350ed72 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php @@ -12,6 +12,9 @@ use Magento\Quote\Model\QuoteAddressValidator; use Magento\Customer\Api\AddressRepositoryInterface; +/** + * Saves billing address for quotes. + */ class BillingAddressPersister { /** @@ -37,6 +40,8 @@ public function __construct( } /** + * Save address for billing. + * * @param CartInterface $quote * @param AddressInterface $address * @param bool $useForShipping @@ -47,7 +52,7 @@ public function __construct( public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $customerAddressId = $address->getCustomerAddressId(); $shippingAddress = null; $addressData = []; diff --git a/app/code/Magento/Quote/Model/Quote/Address/Item.php b/app/code/Magento/Quote/Model/Quote/Address/Item.php index d7014403f7884..ade4f9270b68f 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Item.php @@ -8,6 +8,8 @@ use Magento\Quote\Model\Quote; /** + * Quote item model. + * * @api * @method int getParentItemId() * @method \Magento\Quote\Model\Quote\Address\Item setParentItemId(int $value) @@ -46,6 +48,8 @@ * @method \Magento\Quote\Model\Quote\Address\Item setSuperProductId(int $value) * @method int getParentProductId() * @method \Magento\Quote\Model\Quote\Address\Item setParentProductId(int $value) + * @method int getStoreId() + * @method \Magento\Quote\Model\Quote\Address\Item setStoreId(int $value) * @method string getSku() * @method \Magento\Quote\Model\Quote\Address\Item setSku(string $value) * @method string getImage() @@ -101,7 +105,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem protected $_quote; /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -109,7 +113,7 @@ protected function _construct() } /** - * @return $this|\Magento\Quote\Model\Quote\Item\AbstractItem + * @inheritdoc */ public function beforeSave() { @@ -154,6 +158,8 @@ public function getQuote() } /** + * Import quote item. + * * @param \Magento\Quote\Model\Quote\Item $quoteItem * @return $this */ @@ -168,6 +174,8 @@ public function importQuoteItem(\Magento\Quote\Model\Quote\Item $quoteItem) $quoteItem->getProductId() )->setProduct( $quoteItem->getProduct() + )->setStoreId( + $quoteItem->getStoreId() )->setSku( $quoteItem->getSku() )->setName( @@ -190,10 +198,9 @@ public function importQuoteItem(\Magento\Quote\Model\Quote\Item $quoteItem) } /** - * @param string $code - * @return \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface|null + * @inheritdoc */ - public function getOptionBycode($code) + public function getOptionByCode($code) { if ($this->getQuoteItem()) { return $this->getQuoteItem()->getOptionBycode($code); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php index 42224c970ed27..00060c15c10d8 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php @@ -6,6 +6,8 @@ namespace Magento\Quote\Model\Quote\Address; /** + * Class Total + * * @method string getCode() * * @api @@ -54,6 +56,8 @@ public function __construct( */ public function setTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->totalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -72,6 +76,8 @@ public function setTotalAmount($code, $amount) */ public function setBaseTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->baseTotalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -167,6 +173,7 @@ public function getAllBaseTotalAmounts() /** * Set the full info, which is used to capture tax related information. + * * If a string is used, it is assumed to be serialized. * * @param array|string $info diff --git a/app/code/Magento/Quote/Model/Quote/Item/Compare.php b/app/code/Magento/Quote/Model/Quote/Item/Compare.php index ddaa636ef32b3..76ba324518dc1 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Compare.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Compare.php @@ -50,7 +50,7 @@ protected function getOptionValues($value) if (is_string($value) && $this->jsonValidator->isValid($value)) { $value = $this->serializer->unserialize($value); if (is_array($value)) { - unset($value['qty'], $value['uenc']); + unset($value['qty'], $value['uenc'], $value['related_product'], $value['item']); $value = array_filter($value, function ($optionValue) { return !empty($optionValue); }); diff --git a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php index 32687499274f8..6192d3471ccb0 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php +++ b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php @@ -48,6 +48,8 @@ public function __construct( } /** + * Convert quote item(quote address item) into order item. + * * @param Item|AddressItem $item * @param array $data * @return OrderItemInterface @@ -63,6 +65,16 @@ public function convert($item, $data = []) 'to_order_item', $item ); + if ($item instanceof \Magento\Quote\Model\Quote\Address\Item) { + $orderItemData = array_merge( + $orderItemData, + $this->objectCopyService->getDataFromFieldset( + 'quote_convert_address_item', + 'to_order_item', + $item + ) + ); + } if (!$item->getNoDiscount()) { $data = array_merge( $data, diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index 9a86829bfc4ce..e7750f5879de5 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -6,10 +6,13 @@ namespace Magento\Quote\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; /** * Quote shipping/billing address validator service. * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteAddressValidator { @@ -28,7 +31,7 @@ class QuoteAddressValidator protected $customerRepository; /** - * @var \Magento\Customer\Model\Session + * @deprecated This class is not a part of HTML presentation layer and should not use sessions. */ protected $customerSession; @@ -50,44 +53,80 @@ public function __construct( } /** - * Validates the fields in a specified address data object. + * Validate address. * - * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. - * @return bool + * @param AddressInterface $address + * @param int|null $customerId Cart belongs to + * @return void * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. */ - public function validate(\Magento\Quote\Api\Data\AddressInterface $addressData) + private function doValidate(AddressInterface $address, ?int $customerId): void { //validate customer id - if ($addressData->getCustomerId()) { - $customer = $this->customerRepository->getById($addressData->getCustomerId()); + if ($customerId) { + $customer = $this->customerRepository->getById($customerId); if (!$customer->getId()) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer id %1', $addressData->getCustomerId()) + __('Invalid customer id %1', $customerId) ); } } - if ($addressData->getCustomerAddressId()) { + if ($address->getCustomerAddressId()) { + //Existing address cannot belong to a guest + if (!$customerId) { + throw new \Magento\Framework\Exception\NoSuchEntityException( + __('Invalid customer address id %1', $address->getCustomerAddressId()) + ); + } + //Validating address ID try { - $this->addressRepository->getById($addressData->getCustomerAddressId()); + $this->addressRepository->getById($address->getCustomerAddressId()); } catch (NoSuchEntityException $e) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid address id %1', $addressData->getId()) + __('Invalid address id %1', $address->getId()) ); } - + //Finding available customer's addresses $applicableAddressIds = array_map(function ($address) { /** @var \Magento\Customer\Api\Data\AddressInterface $address */ return $address->getId(); - }, $this->customerRepository->getById($addressData->getCustomerId())->getAddresses()); - if (!in_array($addressData->getCustomerAddressId(), $applicableAddressIds)) { + }, $this->customerRepository->getById($customerId)->getAddresses()); + if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer address id %1', $addressData->getCustomerAddressId()) + __('Invalid customer address id %1', $address->getCustomerAddressId()) ); } } + } + + /** + * Validates the fields in a specified address data object. + * + * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. + * @return bool + * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. + * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + */ + public function validate(AddressInterface $addressData) + { + $this->doValidate($addressData, $addressData->getCustomerId()); + return true; } + + /** + * Validate address to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. + * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + */ + public function validateForCart(CartInterface $cart, AddressInterface $address): void + { + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + } } diff --git a/app/code/Magento/Quote/Model/QuoteIdMask.php b/app/code/Magento/Quote/Model/QuoteIdMask.php index 7950ab47c3665..47b02db7650df 100644 --- a/app/code/Magento/Quote/Model/QuoteIdMask.php +++ b/app/code/Magento/Quote/Model/QuoteIdMask.php @@ -53,11 +53,14 @@ protected function _construct() * Initialize quote identifier before save * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function beforeSave() { parent::beforeSave(); - $this->setMaskedId($this->randomDataGenerator->getUniqueHash()); + if (empty($this->getMaskedId())) { + $this->setMaskedId($this->randomDataGenerator->getUniqueHash()); + } return $this; } } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 6ed8393f80658..0ad99ffe759f6 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -25,6 +25,7 @@ /** * Class QuoteManagement * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -145,6 +146,16 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface */ private $addressesToSync = []; + /** + * @var \Magento\Framework\App\RequestInterface + */ + private $request; + + /** + * @var \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress + */ + private $remoteAddress; + /** * @param EventManager $eventManager * @param QuoteValidator $quoteValidator @@ -168,6 +179,8 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface * @param QuoteFactory $quoteFactory * @param \Magento\Quote\Model\QuoteIdMaskFactory|null $quoteIdMaskFactory * @param \Magento\Customer\Api\AddressRepositoryInterface|null $addressRepository + * @param \Magento\Framework\App\RequestInterface|null $request + * @param \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress $remoteAddress * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -192,7 +205,9 @@ public function __construct( \Magento\Customer\Api\AccountManagementInterface $accountManagement, \Magento\Quote\Model\QuoteFactory $quoteFactory, \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory = null, - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null + \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null, + \Magento\Framework\App\RequestInterface $request = null, + \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress $remoteAddress = null ) { $this->eventManager = $eventManager; $this->quoteValidator = $quoteValidator; @@ -218,6 +233,10 @@ public function __construct( ->get(\Magento\Quote\Model\QuoteIdMaskFactory::class); $this->addressRepository = $addressRepository ?: ObjectManager::getInstance() ->get(\Magento\Customer\Api\AddressRepositoryInterface::class); + $this->request = $request ?: ObjectManager::getInstance() + ->get(\Magento\Framework\App\RequestInterface::class); + $this->remoteAddress = $remoteAddress ?: ObjectManager::getInstance() + ->get(\Magento\Framework\HTTP\PhpEnvironment\RemoteAddress::class); } /** @@ -280,6 +299,7 @@ public function assignCustomer($cartId, $customerId, $storeId) throw new StateException( __("The customer can't be assigned to the cart because the customer already has an active cart.") ); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } @@ -356,10 +376,25 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) if ($quote->getCheckoutMethod() === self::METHOD_GUEST) { $quote->setCustomerId(null); $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); + if ($quote->getCustomerFirstname() === null && $quote->getCustomerLastname() === null) { + $quote->setCustomerFirstname($quote->getBillingAddress()->getFirstname()); + $quote->setCustomerLastname($quote->getBillingAddress()->getLastname()); + if ($quote->getBillingAddress()->getMiddlename() === null) { + $quote->setCustomerMiddlename($quote->getBillingAddress()->getMiddlename()); + } + } $quote->setCustomerIsGuest(true); $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); } + $remoteAddress = $this->remoteAddress->getRemoteAddress(); + if ($remoteAddress !== false) { + $quote->setRemoteIp($remoteAddress); + $quote->setXForwardedFor( + $this->request->getServer('HTTP_X_FORWARDED_FOR') + ); + } + $this->eventManager->dispatch('checkout_submit_before', ['quote' => $quote]); $order = $this->submit($quote); @@ -524,19 +559,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $this->quoteRepository->save($quote); } catch (\Exception $e) { - if (!empty($this->addressesToSync)) { - foreach ($this->addressesToSync as $addressId) { - $this->addressRepository->deleteById($addressId); - } - } - $this->eventManager->dispatch( - 'sales_model_service_quote_submit_failure', - [ - 'order' => $order, - 'quote' => $quote, - 'exception' => $e - ] - ); + $this->rollbackAddresses($quote, $order, $e); throw $e; } return $order; @@ -603,4 +626,43 @@ protected function _prepareCustomerQuote($quote) $shipping->setIsDefaultBilling(true); } } + + /** + * Remove related to order and quote addresses and submit exception to further processing. + * + * @param Quote $quote + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Exception $e + * @throws \Exception + */ + private function rollbackAddresses( + QuoteEntity $quote, + \Magento\Sales\Api\Data\OrderInterface $order, + \Exception $e + ): void { + try { + if (!empty($this->addressesToSync)) { + foreach ($this->addressesToSync as $addressId) { + $this->addressRepository->deleteById($addressId); + } + } + $this->eventManager->dispatch( + 'sales_model_service_quote_submit_failure', + [ + 'order' => $order, + 'quote' => $quote, + 'exception' => $e, + ] + ); + // phpcs:ignore Magento2.Exceptions.ThrowCatch + } catch (\Exception $consecutiveException) { + $message = sprintf( + "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s", + $consecutiveException->getMessage() + ); + + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception($message, 0, $e); + } + } } diff --git a/app/code/Magento/Quote/Model/QuoteValidator.php b/app/code/Magento/Quote/Model/QuoteValidator.php index 062cf76bcaa1a..e67a0f1356262 100644 --- a/app/code/Magento/Quote/Model/QuoteValidator.php +++ b/app/code/Magento/Quote/Model/QuoteValidator.php @@ -25,7 +25,7 @@ class QuoteValidator /** * Maximum available number */ - const MAXIMUM_AVAILABLE_NUMBER = 99999999; + const MAXIMUM_AVAILABLE_NUMBER = 10000000000000000; /** * @var AllowedCountries diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 946c0e0c5f3b8..ae26407c74522 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -23,8 +23,8 @@ class Quote extends AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Snapshot $entitySnapshot, - * @param RelationComposite $entityRelationComposite, + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite * @param \Magento\SalesSequence\Model\Manager $sequenceManager * @param string $connectionName */ @@ -296,7 +296,7 @@ public function markQuotesRecollect($productIds) } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index abecec395865d..392a815ed963c 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -256,9 +256,8 @@ protected function _assignProducts(): self foreach ($this as $item) { /** @var ProductInterface $product */ $product = $productCollection->getItemById($item->getProductId()); - $isValidProduct = $this->isValidProduct($product); $qtyOptions = []; - if ($isValidProduct) { + if ($product && $this->isValidProduct($product)) { $product->setCustomOptions([]); $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection); foreach ($optionProductIds as $optionProductId) { diff --git a/app/code/Magento/Quote/Model/ShippingAddressManagement.php b/app/code/Magento/Quote/Model/ShippingAddressManagement.php index d8e70c68ba33f..b9edcc13d0077 100644 --- a/app/code/Magento/Quote/Model/ShippingAddressManagement.php +++ b/app/code/Magento/Quote/Model/ShippingAddressManagement.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritDoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address) @@ -95,7 +95,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0; $sameAsBilling = $address->getSameAsBilling() ? 1 : 0; $customerAddressId = $address->getCustomerAddressId(); - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $quote->setShippingAddress($address); $address = $quote->getShippingAddress(); @@ -123,7 +123,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres } /** - * {@inheritDoc} + * @inheritDoc */ public function get($cartId) { diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index 76c9a49b290c5..77dfec9603a5c 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -7,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Handle customer VAT number on collect_totals_before event of quote address. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CollectTotalsObserver implements ObserverInterface { /** @@ -124,7 +129,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); } - if ($groupId) { + if ($groupId !== null) { $address->setPrevQuoteCustomerGroupId($quote->getCustomerGroupId()); $quote->setCustomerGroupId($groupId); $this->customerSession->setCustomerGroupId($groupId); diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php new file mode 100644 index 0000000000000..19a7e03264d8a --- /dev/null +++ b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Plugin; + +use Magento\Checkout\Model\Session; +use Magento\Quote\Model\QuoteRepository; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcherInterface; + +/** + * Updates quote items store id. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class UpdateQuoteItemStore +{ + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @param QuoteRepository $quoteRepository + * @param Session $checkoutSession + */ + public function __construct( + QuoteRepository $quoteRepository, + Session $checkoutSession + ) { + $this->quoteRepository = $quoteRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Update store id in active quote after store view switching. + * + * @param StoreSwitcherInterface $subject + * @param string $result + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSwitch( + StoreSwitcherInterface $subject, + $result, + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ): string { + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + $quote->setStoreId( + $targetStore->getId() + ); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + return $result; + } +} diff --git a/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php b/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php index f537280272227..6c23379a37cf0 100644 --- a/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php +++ b/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php @@ -3,18 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Quote\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; use Magento\Quote\Setup\ConvertSerializedDataToJsonFactory; use Magento\Quote\Setup\QuoteSetupFactory; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class ConvertSerializedDataToJson - * @package Magento\Quote\Setup\Patch + * Convert quote serialized data to json. */ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInterface { @@ -36,6 +33,8 @@ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInt /** * PatchInitial constructor. * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + * @param QuoteSetupFactory $quoteSetupFactory + * @param ConvertSerializedDataToJsonFactory $convertSerializedDataToJsonFactory */ public function __construct( \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup, @@ -48,7 +47,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -57,7 +56,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -67,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -75,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index ab0db2dac643e..4ec608a18a686 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -22,6 +22,9 @@ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> <!-- Create the configurable product based on the data in the /data folder --> <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> <requiredEntity createDataKey="createCategory"/> @@ -73,21 +76,22 @@ </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> </after> - <!-- Step 1: Add simple product to shopping cart --> + <!-- Step 1: Add simple product to shopping cart --> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart"> <argument name="product" value="$$createSimpleProduct$$"/> <argument name="productCount" value="1"/> </actionGroup> <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage"/> - <waitForPageLoad stepKey="waitForStoreFrontLoad"/> <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectOption"/> <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart($$createConfigProduct.name$$)}}" time="30" stepKey="assertMessage"/> @@ -96,8 +100,6 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> <argument name="product" value="$$createConfigChildProduct1$$"/> </actionGroup> @@ -106,18 +108,44 @@ <waitForPageLoad stepKey="waitForProductPageLoad"/> <!-- Disabled child configurable product --> <click selector="{{AdminProductFormSection.enableProductAttributeLabel}}" stepKey="clickDisableProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> - <waitForPageLoad stepKey="waitForProductPageSaved"/> + <actionGroup ref="saveProductForm" stepKey="clickSaveProduct"/> <!-- Disabled simple product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> <argument name="product" value="$$createSimpleProduct$$"/> <argument name="status" value="Disable"/> </actionGroup> <closeTab stepKey="closeTab"/> - <!--Check cart--> + <!-- Check cart --> <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload"/> <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart"/> <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem"/> + <!-- Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct2.name$$)}}" stepKey="amOnSimpleProductPage2"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartPage"/> + <!-- Disabled via admin panel --> + <openNewTab stepKey="openNewTab2"/> + <!-- Find the first simple product that we just created using the product grid and go to its page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <!-- Disabled simple product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab2"/> + <!--Check cart--> + <reloadPage stepKey="reloadPage2"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart2"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem2"/> </test> </tests> diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php index 1e999cb5e523e..804f0863d2d2a 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php @@ -77,7 +77,8 @@ protected function setUp() 'getAllVisibleItems', 'getBaseCurrencyCode', 'getQuoteCurrencyCode', - 'getItemsQty' + 'getItemsQty', + 'collectTotals' ]); $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); $this->addressMock = $this->createPartialMock( diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php index c1c131260f17a..242f81b222507 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php @@ -216,6 +216,7 @@ public function testValidateMinimumAmountVirtual() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; @@ -240,6 +241,31 @@ public function testValidateMinimumAmount() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], + ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], + ]; + + $this->quote->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->quote->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(false); + + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->willReturnMap($scopeConfigValues); + + $this->assertTrue($this->address->validateMinimumAmount()); + } + + public function testValidateMiniumumAmountWithoutDiscount() + { + $storeId = 1; + $scopeConfigValues = [ + ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], + ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, false], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; @@ -263,6 +289,7 @@ public function testValidateMinimumAmountNegative() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php deleted file mode 100644 index 08f5f6a808561..0000000000000 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Quote\Test\Unit\Model; - -class QuoteAddressValidatorTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Quote\Model\QuoteAddressValidator - */ - protected $model; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $addressRepositoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerRepositoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteAddressMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerSessionMock; - - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->quoteAddressMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Address::class, - ['getCustomerId', 'load', 'getId', '__wakeup'] - ); - $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->model = $this->objectManager->getObject( - \Magento\Quote\Model\QuoteAddressValidator::class, - [ - 'addressRepository' => $this->addressRepositoryMock, - 'customerRepository' => $this->customerRepositoryMock, - 'customerSession' => $this->customerSessionMock - ] - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid customer id 100 - */ - public function testValidateInvalidCustomer() - { - $customerId = 100; - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - - $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId); - $this->customerRepositoryMock->expects($this->once())->method('getById')->with($customerId) - ->willReturn($customerMock); - $this->model->validate($address); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid address id 101 - */ - public function testValidateInvalidAddress() - { - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $this->customerRepositoryMock->expects($this->never())->method('getById'); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn(101); - $address->expects($this->once())->method('getId')->willReturn(101); - - $this->addressRepositoryMock->expects($this->once())->method('getById') - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); - - $this->model->validate($address); - } - - /** - * Neither customer id used nor address id exists - */ - public function testValidateNewAddress() - { - $this->customerRepositoryMock->expects($this->never())->method('getById'); - $this->addressRepositoryMock->expects($this->never())->method('getById'); - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $this->assertTrue($this->model->validate($address)); - } - - public function testValidateWithValidAddress() - { - $addressCustomer = 100; - $customerAddressId = 42; - - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($addressCustomer); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn($customerAddressId); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $customerAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - - $this->customerRepositoryMock->expects($this->exactly(2))->method('getById')->willReturn($customerMock); - $customerMock->expects($this->once())->method('getId')->willReturn($addressCustomer); - - $this->addressRepositoryMock->expects($this->once())->method('getById')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->any())->method('getCustomerId')->willReturn($addressCustomer); - - $customerMock->expects($this->once())->method('getAddresses')->willReturn([$customerAddress]); - $customerAddress->expects($this->once())->method('getId')->willReturn(42); - - $this->assertTrue($this->model->validate($address)); - } -} diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index 72e516e35cd6e..8d8200cd6ef62 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -6,9 +6,12 @@ namespace Magento\Quote\Test\Unit\Model; +use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Quote\Model\CustomerManagement; +use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Sales\Api\Data\OrderAddressInterface; /** @@ -137,6 +140,21 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase */ private $quoteFactoryMock; + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress|\PHPUnit_Framework_MockObject_MockObject + */ + private $remoteAddressMock; + + /** + * @var \Magento\Quote\Model\QuoteIdMaskFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteIdMaskFactoryMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -178,18 +196,20 @@ protected function setUp() ); $this->quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, [ - 'getId', - 'getCheckoutMethod', - 'setCheckoutMethod', - 'setCustomerId', - 'setCustomerEmail', - 'getBillingAddress', - 'setCustomerIsGuest', - 'setCustomerGroupId', - 'assignCustomer', - 'getPayment', - 'collectTotals' - ]); + 'assignCustomer', + 'collectTotals', + 'getBillingAddress', + 'getCheckoutMethod', + 'getPayment', + 'setCheckoutMethod', + 'setCustomerEmail', + 'setCustomerGroupId', + 'setCustomerId', + 'setCustomerIsGuest', + 'setRemoteIp', + 'setXForwardedFor', + 'getId', + ]); $this->quoteAddressFactory = $this->createPartialMock( \Magento\Quote\Model\Quote\AddressFactory::class, @@ -237,8 +257,11 @@ protected function setUp() // Set the new dependency $this->quoteIdMock = $this->createMock(\Magento\Quote\Model\QuoteIdMask::class); - $quoteIdFactoryMock = $this->createPartialMock(\Magento\Quote\Model\QuoteIdMaskFactory::class, ['create']); - $this->setPropertyValue($this->model, 'quoteIdMaskFactory', $quoteIdFactoryMock); + $this->quoteIdMaskFactoryMock = $this->createPartialMock(QuoteIdMaskFactory::class, ['create']); + $this->setPropertyValue($this->model, 'quoteIdMaskFactory', $this->quoteIdMaskFactoryMock); + + $this->requestMock = $this->createPartialMockForAbstractClass(RequestInterface::class, ['getServer']); + $this->remoteAddressMock = $this->createMock(RemoteAddress::class); } public function testCreateEmptyCartAnonymous() @@ -645,7 +668,7 @@ public function testPlaceOrderIfCustomerIsGuest() $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getEmail']); $addressMock->expects($this->once())->method('getEmail')->willReturn($email); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->with()->willReturn($addressMock); + $this->quoteMock->expects($this->any())->method('getBillingAddress')->with()->willReturn($addressMock); $this->quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(true)->willReturnSelf(); $this->quoteMock->expects($this->once()) @@ -676,7 +699,11 @@ public function testPlaceOrderIfCustomerIsGuest() 'checkoutSession' => $this->checkoutSessionMock, 'customerSession' => $this->customerSessionMock, 'accountManagement' => $this->accountManagementMock, - 'quoteFactory' => $this->quoteFactoryMock + 'quoteFactory' => $this->quoteFactoryMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'addressRepository' => $this->addressRepositoryMock, + 'request' => $this->requestMock, + 'remoteAddress' => $this->remoteAddressMock, ] ) ->getMock(); @@ -709,13 +736,15 @@ public function testPlaceOrder() $orderId = 332; $orderIncrementId = 100003332; $orderStatus = 'status1'; + $remoteAddress = '192.168.1.10'; + $forwardedForIp = '192.168.1.11'; /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\QuoteManagement $service */ $service = $this->getMockBuilder(\Magento\Quote\Model\QuoteManagement::class) ->setMethods(['submit']) ->setConstructorArgs( [ - 'eventManager' => $this->eventManager, + 'eventManager' => $this->eventManager, 'quoteValidator' => $this->quoteValidator, 'orderFactory' => $this->orderFactory, 'orderManagement' => $this->orderManagement, @@ -734,7 +763,11 @@ public function testPlaceOrder() 'checkoutSession' => $this->checkoutSessionMock, 'customerSession' => $this->customerSessionMock, 'accountManagement' => $this->accountManagementMock, - 'quoteFactory' => $this->quoteFactoryMock + 'quoteFactory' => $this->quoteFactoryMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'addressRepository' => $this->addressRepositoryMock, + 'request' => $this->requestMock, + 'remoteAddress' => $this->remoteAddressMock, ] ) ->getMock(); @@ -762,6 +795,17 @@ public function testPlaceOrder() ->method('setCustomerIsGuest') ->with(true); + $this->remoteAddressMock + ->method('getRemoteAddress') + ->willReturn($remoteAddress); + + $this->requestMock + ->method('getServer') + ->willReturn($forwardedForIp); + + $this->quoteMock->expects($this->once())->method('setRemoteIp')->with($remoteAddress); + $this->quoteMock->expects($this->once())->method('setXForwardedFor')->with($forwardedForIp); + $service->expects($this->once())->method('submit')->willReturn($orderMock); $this->quoteMock->expects($this->atLeastOnce())->method('getId')->willReturn($cartId); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index 22785f051dcfa..07e203f71714d 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -975,6 +975,7 @@ public function testValidateMinimumAmount() ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; $this->scopeConfig->expects($this->any()) @@ -1001,6 +1002,7 @@ public function testValidateMinimumAmountNegative() ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; $this->scopeConfig->expects($this->any()) diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php deleted file mode 100644 index 89fea2bec73a8..0000000000000 --- a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php +++ /dev/null @@ -1,282 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Quote\Test\Unit\Model; - -use \Magento\Quote\Model\ShippingAddressManagement; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ShippingAddressManagementTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ShippingAddressManagement - */ - protected $service; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteRepositoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteAddressMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $validatorMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $scopeConfigMock; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $totalsCollectorMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $addressRepository; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $amountErrorMessageMock; - - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); - $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - - $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ - 'setSameAsBilling', - 'setCollectShippingRates', - '__wakeup', - 'collectTotals', - 'save', - 'getId', - 'getCustomerAddressId', - 'getSaveInAddressBook', - 'getSameAsBilling', - 'importCustomerAddressData', - 'setSaveInAddressBook', - ]); - $this->validatorMock = $this->createMock(\Magento\Quote\Model\QuoteAddressValidator::class); - $this->totalsCollectorMock = $this->createMock(\Magento\Quote\Model\Quote\TotalsCollector::class); - $this->addressRepository = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - - $this->amountErrorMessageMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage::class, - ['getMessage'] - ); - - $this->service = $this->objectManager->getObject( - \Magento\Quote\Model\ShippingAddressManagement::class, - [ - 'quoteRepository' => $this->quoteRepositoryMock, - 'addressValidator' => $this->validatorMock, - 'logger' => $this->createMock(\Psr\Log\LoggerInterface::class), - 'scopeConfig' => $this->scopeConfigMock, - 'totalsCollector' => $this->totalsCollectorMock, - 'addressRepository' => $this->addressRepository - ] - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage error345 - */ - public function testSetAddressValidationFailed() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart654') - ->will($this->returnValue($quoteMock)); - - $this->validatorMock->expects($this->once())->method('validate') - ->will($this->throwException(new \Magento\Framework\Exception\NoSuchEntityException(__('error345')))); - - $this->service->assign('cart654', $this->quoteAddressMock); - } - - public function testSetAddress() - { - $addressId = 1; - $customerAddressId = 150; - - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] - ); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(false)); - $quoteMock->expects($this->once()) - ->method('setShippingAddress') - ->with($this->quoteAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getSameAsBilling')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getCustomerAddressId')->willReturn($customerAddressId); - - $customerAddressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - - $this->addressRepository->expects($this->once()) - ->method('getById') - ->with($customerAddressId) - ->willReturn($customerAddressMock); - - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) - ->willReturn(true); - - $quoteMock->expects($this->exactly(3))->method('getShippingAddress')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->once()) - ->method('importCustomerAddressData') - ->with($customerAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('setSameAsBilling')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('setSaveInAddressBook')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once()) - ->method('setCollectShippingRates') - ->with(true) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('save')->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('getId')->will($this->returnValue($addressId)); - - $this->assertEquals($addressId, $this->service->assign('cart867', $this->quoteAddressMock)); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. - */ - public function testSetAddressForVirtualProduct() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->will($this->returnValue($quoteMock)); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(true)); - $quoteMock->expects($this->never())->method('setShippingAddress'); - - $this->quoteAddressMock->expects($this->never())->method('getCustomerAddressId'); - $this->quoteAddressMock->expects($this->never())->method('setSaveInAddressBook'); - - $quoteMock->expects($this->never())->method('save'); - - $this->service->assign('cart867', $this->quoteAddressMock); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage The address failed to save. Verify the address and try again. - */ - public function testSetAddressWithInabilityToSaveQuote() - { - $this->quoteAddressMock->expects($this->once())->method('save')->willThrowException( - new \Exception('The address failed to save. Verify the address and try again.') - ); - - $customerAddressId = 150; - - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] - ); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(false)); - $quoteMock->expects($this->once()) - ->method('setShippingAddress') - ->with($this->quoteAddressMock) - ->willReturnSelf(); - - $customerAddressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - - $this->addressRepository->expects($this->once()) - ->method('getById') - ->with($customerAddressId) - ->willReturn($customerAddressMock); - - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) - ->willReturn(true); - - $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getSameAsBilling')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getCustomerAddressId')->willReturn($customerAddressId); - - $quoteMock->expects($this->exactly(2))->method('getShippingAddress')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->once()) - ->method('importCustomerAddressData') - ->with($customerAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('setSameAsBilling')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('setSaveInAddressBook')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once()) - ->method('setCollectShippingRates') - ->with(true) - ->willReturnSelf(); - - $this->service->assign('cart867', $this->quoteAddressMock); - } - - public function testGetAddress() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with('cartId')->will( - $this->returnValue($quoteMock) - ); - - $addressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $quoteMock->expects($this->any())->method('getShippingAddress')->will($this->returnValue($addressMock)); - $quoteMock->expects($this->any())->method('isVirtual')->will($this->returnValue(false)); - $this->assertEquals($addressMock, $this->service->get('cartId')); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. - */ - public function testGetAddressOfQuoteWithVirtualProducts() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with('cartId')->will( - $this->returnValue($quoteMock) - ); - - $quoteMock->expects($this->any())->method('isVirtual')->will($this->returnValue(true)); - $quoteMock->expects($this->never())->method('getShippingAddress'); - - $this->service->get('cartId'); - } -} diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index f3357f8aacd31..4ea067c9be8f6 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -199,7 +199,7 @@ public function testDispatchWithCustomerCountryNotInEUAndNotLoggedCustomerInGrou ->method('getNotLoggedInGroup') ->will($this->returnValue($this->groupInterfaceMock)); $this->groupInterfaceMock->expects($this->once()) - ->method('getId')->will($this->returnValue(0)); + ->method('getId')->will($this->returnValue(null)); $this->vatValidatorMock->expects($this->once()) ->method('isEnabled') ->with($this->quoteAddressMock, $this->storeId) @@ -220,9 +220,6 @@ public function testDispatchWithCustomerCountryNotInEUAndNotLoggedCustomerInGrou $this->returnValue(false) ); - $groupMock = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) - ->disableOriginalConstructor() - ->getMock(); $this->customerMock->expects($this->once())->method('getId')->will($this->returnValue(null)); /** Assertions */ diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml index 7dd215b87e8cc..48954f1af90fc 100644 --- a/app/code/Magento/Quote/etc/db_schema.xml +++ b/app/code/Magento/Quote/etc/db_schema.xml @@ -37,9 +37,9 @@ comment="Store Currency Code"/> <column xsi:type="varchar" name="quote_currency_code" nullable="true" length="255" comment="Quote Currency Code"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Grand Total"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Grand Total"/> <column xsi:type="varchar" name="checkout_method" nullable="true" length="255" comment="Checkout Method"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" @@ -68,19 +68,19 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="255" comment="Coupon Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="255" comment="Global Currency Code"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="base_to_quote_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_quote_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Quote Rate"/> <column xsi:type="varchar" name="customer_taxvat" nullable="true" length="255" comment="Customer Taxvat"/> <column xsi:type="varchar" name="customer_gender" nullable="true" length="255" comment="Customer Gender"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal With Discount"/> - <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal With Discount"/> <column xsi:type="int" name="is_changed" padding="10" unsigned="true" nullable="true" identity="false" comment="Is Changed"/> @@ -143,57 +143,57 @@ comment="Shipping Description"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Weight"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Subtotal"/> - <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal With Discount"/> - <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Subtotal With Discount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Shipping Amount"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Grand Total"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Grand Total"/> <column xsi:type="text" name="customer_notes" nullable="true" comment="Customer Notes"/> <column xsi:type="text" name="applied_taxes" nullable="true" comment="Applied Taxes"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> - <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="base_subtotal_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> <column xsi:type="text" name="vat_id" nullable="true" comment="Vat Id"/> <column xsi:type="smallint" name="vat_is_valid" padding="6" unsigned="false" nullable="true" identity="false" @@ -202,6 +202,8 @@ <column xsi:type="text" name="vat_request_date" nullable="true" comment="Vat Request Date"/> <column xsi:type="smallint" name="vat_request_success" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Request Success"/> + <column xsi:type="text" name="validated_country_code" nullable="true" comment="Validated Country Code"/> + <column xsi:type="text" name="validated_vat_number" nullable="true" comment="Validated Vat Number"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="address_id"/> </constraint> @@ -249,45 +251,45 @@ comment="Custom Price"/> <column xsi:type="decimal" name="discount_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Discount Percent"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Amount"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Tax Percent"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Total"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Total"/> - <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Row Total With Discount"/> <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> <column xsi:type="varchar" name="product_type" nullable="true" length="255" comment="Product Type"/> - <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Before Discount"/> - <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> <column xsi:type="decimal" name="original_custom_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Original Custom Price"/> <column xsi:type="varchar" name="redirect_url" nullable="true" length="255" comment="Redirect Url"/> <column xsi:type="decimal" name="base_cost" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Cost"/> - <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> - <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price Incl Tax"/> - <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> - <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="item_id"/> @@ -330,19 +332,19 @@ comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Total"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Total"/> - <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Row Total With Discount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Amount"/> <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> @@ -352,6 +354,8 @@ comment="Super Product Id"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Parent Product Id"/> + <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" + comment="Store Id"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> <column xsi:type="varchar" name="image" nullable="true" length="255" comment="Image"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> @@ -370,17 +374,17 @@ comment="Base Price"/> <column xsi:type="decimal" name="base_cost" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Cost"/> - <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> - <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price Incl Tax"/> - <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> - <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="address_item_id"/> @@ -403,6 +407,9 @@ <index referenceId="QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID" indexType="btree"> <column name="quote_item_id"/> </index> + <index referenceId="QUOTE_ADDRESS_ITEM_STORE_ID" indexType="btree"> + <column name="store_id"/> + </index> </table> <table name="quote_item_option" resource="checkout" engine="innodb" comment="Sales Flat Quote Item Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" @@ -472,7 +479,7 @@ <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> <column xsi:type="varchar" name="method" nullable="true" length="255" comment="Method"/> <column xsi:type="text" name="method_description" nullable="true" comment="Method Description"/> - <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" + <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Price"/> <column xsi:type="text" name="error_message" nullable="true" comment="Error Message"/> <column xsi:type="text" name="method_title" nullable="true" comment="Method Title"/> diff --git a/app/code/Magento/Quote/etc/db_schema_whitelist.json b/app/code/Magento/Quote/etc/db_schema_whitelist.json index c2cc34293dcb5..5667a9a5b4600 100644 --- a/app/code/Magento/Quote/etc/db_schema_whitelist.json +++ b/app/code/Magento/Quote/etc/db_schema_whitelist.json @@ -212,6 +212,7 @@ "product_id": true, "super_product_id": true, "parent_product_id": true, + "store_id": true, "sku": true, "image": true, "name": true, @@ -233,7 +234,8 @@ "index": { "QUOTE_ADDRESS_ITEM_QUOTE_ADDRESS_ID": true, "QUOTE_ADDRESS_ITEM_PARENT_ITEM_ID": true, - "QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID": true + "QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID": true, + "QUOTE_ADDRESS_ITEM_STORE_ID": true }, "constraint": { "PRIMARY": true, diff --git a/app/code/Magento/Quote/etc/fieldset.xml b/app/code/Magento/Quote/etc/fieldset.xml index 55ec76a647fcd..85ee20c7f8520 100644 --- a/app/code/Magento/Quote/etc/fieldset.xml +++ b/app/code/Magento/Quote/etc/fieldset.xml @@ -186,6 +186,11 @@ <aspect name="to_order_address" /> </field> </fieldset> + <fieldset id="quote_convert_address_item"> + <field name="quote_item_id"> + <aspect name="to_order_item" /> + </field> + </fieldset> <fieldset id="quote_convert_item"> <field name="sku"> <aspect name="to_order_item" /> diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml index 125afb96f20fd..ecad94fbbc249 100644 --- a/app/code/Magento/Quote/etc/frontend/di.xml +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -12,6 +12,9 @@ <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> </arguments> </type> + <type name="Magento\Store\Model\StoreSwitcherInterface"> + <plugin name="update_quote_item_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteItemStore"/> + </type> <type name="Magento\Store\Api\StoreCookieManagerInterface"> <plugin name="update_quote_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteStore"/> </type> diff --git a/app/code/Magento/Quote/etc/sales.xml b/app/code/Magento/Quote/etc/sales.xml index 3d54a6375c8d9..3db72a1226236 100644 --- a/app/code/Magento/Quote/etc/sales.xml +++ b/app/code/Magento/Quote/etc/sales.xml @@ -9,7 +9,7 @@ <section name="quote"> <group name="totals"> <item name="subtotal" instance="Magento\Quote\Model\Quote\Address\Total\Subtotal" sort_order="100"/> - <item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="250"/> + <item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="350"/> <item name="grand_total" instance="Magento\Quote\Model\Quote\Address\Total\Grand" sort_order="550"/> </group> </section> diff --git a/app/code/Magento/Quote/i18n/en_US.csv b/app/code/Magento/Quote/i18n/en_US.csv index ae7453aa0d0cc..b24179297493a 100644 --- a/app/code/Magento/Quote/i18n/en_US.csv +++ b/app/code/Magento/Quote/i18n/en_US.csv @@ -65,3 +65,5 @@ error345,error345 Carts,Carts "Manage carts","Manage carts" "Invalid state change requested","Invalid state change requested" +"Validated Country Code","Validated Country Code" +"Validated Vat Number","Validated Vat Number" diff --git a/app/code/Magento/QuoteAnalytics/composer.json b/app/code/Magento/QuoteAnalytics/composer.json index 90dae1ec2adca..706bed674b4a9 100644 --- a/app/code/Magento/QuoteAnalytics/composer.json +++ b/app/code/Magento/QuoteAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-quote": "*" + "magento/module-quote": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php index 96259f2264943..005cf3a10ca80 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -45,6 +45,8 @@ public function __construct( * @param Quote $cart * @param array $cartItems * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException */ public function execute(Quote $cart, array $cartItems): void { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index aa5b41daebdc3..6868ce3f7f1ff 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -61,11 +61,17 @@ public function __construct( * @return void * @throws GraphQlNoSuchEntityException * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute(Quote $cart, array $cartItemData): void { $sku = $this->extractSku($cartItemData); $qty = $this->extractQty($cartItemData); + if ($qty <= 0) { + throw new GraphQlInputException( + __('Please enter a number greater than 0 in this field.') + ); + } $customizableOptions = $this->extractCustomizableOptions($cartItemData); try { @@ -74,7 +80,16 @@ public function execute(Quote $cart, array $cartItemData): void throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); } - $result = $cart->addProduct($product, $this->createBuyRequest($qty, $customizableOptions)); + try { + $result = $cart->addProduct($product, $this->createBuyRequest($qty, $customizableOptions)); + } catch (\Exception $e) { + throw new GraphQlInputException( + __( + 'Could not add the product with SKU %sku to the shopping cart: %message', + ['sku' => $sku, 'message' => $e->getMessage()] + ) + ); + } if (is_string($result)) { throw new GraphQlInputException(__($result)); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/AddressDataProvider.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/AddressDataProvider.php deleted file mode 100644 index fb742477ec99b..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/AddressDataProvider.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart\Address; - -use Magento\Framework\Api\ExtensibleDataObjectConverter; -use Magento\Quote\Api\Data\AddressInterface; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote\Address as QuoteAddress; - -/** - * Class AddressDataProvider - * - * Collect and return information about cart shipping and billing addresses - */ -class AddressDataProvider -{ - /** - * @var ExtensibleDataObjectConverter - */ - private $dataObjectConverter; - - /** - * AddressDataProvider constructor. - * - * @param ExtensibleDataObjectConverter $dataObjectConverter - */ - public function __construct( - ExtensibleDataObjectConverter $dataObjectConverter - ) { - $this->dataObjectConverter = $dataObjectConverter; - } - - /** - * Collect and return information about shipping and billing addresses - * - * @param CartInterface $cart - * @return array - */ - public function getCartAddresses(CartInterface $cart): array - { - $addressData = []; - $shippingAddress = $cart->getShippingAddress(); - $billingAddress = $cart->getBillingAddress(); - - if ($shippingAddress) { - $shippingData = $this->dataObjectConverter->toFlatArray($shippingAddress, [], AddressInterface::class); - $shippingData['address_type'] = 'SHIPPING'; - $addressData[] = array_merge($shippingData, $this->extractAddressData($shippingAddress)); - } - - if ($billingAddress) { - $billingData = $this->dataObjectConverter->toFlatArray($billingAddress, [], AddressInterface::class); - $billingData['address_type'] = 'BILLING'; - $addressData[] = array_merge($billingData, $this->extractAddressData($billingAddress)); - } - - return $addressData; - } - - /** - * Extract the necessary address fields from address model - * - * @param QuoteAddress $address - * @return array - */ - private function extractAddressData(QuoteAddress $address): array - { - $addressData = [ - 'country' => [ - 'code' => $address->getCountryId(), - 'label' => $address->getCountry() - ], - 'region' => [ - 'code' => $address->getRegionCode(), - 'label' => $address->getRegion() - ], - 'street' => $address->getStreet(), - 'selected_shipping_method' => [ - 'code' => $address->getShippingMethod(), - 'label' => $address->getShippingDescription(), - 'free_shipping' => $address->getFreeShipping(), - ], - 'items_weight' => $address->getWeight(), - 'customer_notes' => $address->getCustomerNotes() - ]; - - return $addressData; - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php new file mode 100644 index 0000000000000..dd6478b4873c6 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignBillingAddressToCart.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\BillingAddressManagementInterface; + +/** + * Set billing address for a specified shopping cart + */ +class AssignBillingAddressToCart +{ + /** + * @var BillingAddressManagementInterface + */ + private $billingAddressManagement; + + /** + * @param BillingAddressManagementInterface $billingAddressManagement + */ + public function __construct( + BillingAddressManagementInterface $billingAddressManagement + ) { + $this->billingAddressManagement = $billingAddressManagement; + } + + /** + * Assign billing address to cart + * + * @param CartInterface $cart + * @param AddressInterface $billingAddress + * @param bool $useForShipping + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute( + CartInterface $cart, + AddressInterface $billingAddress, + bool $useForShipping + ): void { + try { + $this->billingAddressManagement->assign($cart->getId(), $billingAddress, $useForShipping); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php new file mode 100644 index 0000000000000..527999b245a4c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\ShippingAddressManagementInterface; + +/** + * Assign shipping address to cart + */ +class AssignShippingAddressToCart +{ + /** + * @var ShippingAddressManagementInterface + */ + private $shippingAddressManagement; + + /** + * @param ShippingAddressManagementInterface $shippingAddressManagement + */ + public function __construct( + ShippingAddressManagementInterface $shippingAddressManagement + ) { + $this->shippingAddressManagement = $shippingAddressManagement; + } + + /** + * Assign shipping address to cart + * + * @param CartInterface $cart + * @param AddressInterface $shippingAddress + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute( + CartInterface $cart, + AddressInterface $shippingAddress + ): void { + try { + $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php new file mode 100644 index 0000000000000..5b30c0774c22f --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Assign shipping method to cart + */ +class AssignShippingMethodToCart +{ + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingInformationFactory; + + /** + * @var ShippingInformationManagementInterface + */ + private $shippingInformationManagement; + + /** + * @param ShippingInformationInterfaceFactory $shippingInformationFactory + * @param ShippingInformationManagementInterface $shippingInformationManagement + */ + public function __construct( + ShippingInformationInterfaceFactory $shippingInformationFactory, + ShippingInformationManagementInterface $shippingInformationManagement + ) { + $this->shippingInformationFactory = $shippingInformationFactory; + $this->shippingInformationManagement = $shippingInformationManagement; + } + + /** + * Assign shipping method to cart + * + * @param CartInterface $cart + * @param AddressInterface $quoteAddress + * @param string $carrierCode + * @param string $methodCode + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute( + CartInterface $cart, + AddressInterface $quoteAddress, + string $carrierCode, + string $methodCode + ): void { + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingInformationFactory->create([ + 'data' => [ + /* If the address is not a shipping address (but billing) the system will find the proper shipping + address for the selected cart and set the information there (actual for single shipping address) */ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => $carrierCode, + ShippingInformationInterface::SHIPPING_METHOD_CODE => $methodCode, + ], + ]); + + try { + $this->shippingInformationManagement->saveAddressInformation($cart->getId(), $shippingInformation); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForCustomer.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForCustomer.php new file mode 100644 index 0000000000000..481bad090dac1 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForCustomer.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; + +/** + * Create empty cart for customer + */ +class CreateEmptyCartForCustomer +{ + /** + * @var CartManagementInterface + */ + private $cartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + + /** + * @param CartManagementInterface $cartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + */ + public function __construct( + CartManagementInterface $cartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel + ) { + $this->cartManagement = $cartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + } + + /** + * Create empty cart for customer + * + * @param int $customerId + * @param string|null $predefinedMaskedQuoteId + * @return string + */ + public function execute(int $customerId, string $predefinedMaskedQuoteId = null): string + { + $quoteId = $this->cartManagement->createEmptyCartForCustomer($customerId); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quoteId); + + if (isset($predefinedMaskedQuoteId)) { + $quoteIdMask->setMaskedId($predefinedMaskedQuoteId); + } + + $this->quoteIdMaskResourceModel->save($quoteIdMask); + return $quoteIdMask->getMaskedId(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForGuest.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForGuest.php new file mode 100644 index 0000000000000..a6396ed6352ab --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForGuest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; + +/** + * Create empty cart for guest + */ +class CreateEmptyCartForGuest +{ + /** + * @var GuestCartManagementInterface + */ + private $guestCartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + + /** + * @param GuestCartManagementInterface $guestCartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + */ + public function __construct( + GuestCartManagementInterface $guestCartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel + ) { + $this->guestCartManagement = $guestCartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + } + + /** + * Create empty cart for guest + * + * @param string|null $predefinedMaskedQuoteId + * @return string + */ + public function execute(string $predefinedMaskedQuoteId = null): string + { + $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); + + if (isset($predefinedMaskedQuoteId)) { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $this->quoteIdMaskResourceModel->load($quoteIdMask, $maskedQuoteId, 'masked_id'); + + $quoteIdMask->setMaskedId($predefinedMaskedQuoteId); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + } + return $predefinedMaskedQuoteId ?? $maskedQuoteId; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromCart.php deleted file mode 100644 index faefa686606e2..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractDataFromCart.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart; - -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\Item as QuoteItem; - -/** - * Extract data from cart - */ -class ExtractDataFromCart -{ - /** - * Extract data from cart - * - * @param Quote $cart - * @return array - */ - public function execute(Quote $cart): array - { - $items = []; - - /** - * @var QuoteItem $cartItem - */ - foreach ($cart->getAllItems() as $cartItem) { - $productData = $cartItem->getProduct()->getData(); - $productData['model'] = $cartItem->getProduct(); - - $items[] = [ - 'id' => $cartItem->getItemId(), - 'qty' => $cartItem->getQty(), - 'product' => $productData, - 'model' => $cartItem, - ]; - } - - return [ - 'items' => $items, - ]; - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php new file mode 100644 index 0000000000000..840dedb4f274e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Customer\Model\Address\AbstractAddress; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Model\Quote\Address as QuoteAddress; + +/** + * Extract address fields from an Quote Address model + */ +class ExtractQuoteAddressData +{ + /** + * @var ExtensibleDataObjectConverter + */ + private $dataObjectConverter; + + /** + * @param ExtensibleDataObjectConverter $dataObjectConverter + */ + public function __construct(ExtensibleDataObjectConverter $dataObjectConverter) + { + $this->dataObjectConverter = $dataObjectConverter; + } + + /** + * Converts Address model to flat array + * + * @param QuoteAddress $address + * @return array + */ + public function execute(QuoteAddress $address): array + { + $addressData = $this->dataObjectConverter->toFlatArray($address, [], AddressInterface::class); + $addressData['model'] = $address; + + if ($address->getAddressType() == AbstractAddress::TYPE_SHIPPING) { + $addressType = 'SHIPPING'; + } elseif ($address->getAddressType() == AbstractAddress::TYPE_BILLING) { + $addressType = 'BILLING'; + } else { + $addressType = null; + } + + $addressData = array_merge($addressData, [ + 'address_id' => $address->getId(), + 'address_type' => $addressType, + 'country' => [ + 'code' => $address->getCountryId(), + 'label' => $address->getCountry() + ], + 'region' => [ + 'code' => $address->getRegionCode(), + 'label' => $address->getRegion() + ], + 'street' => $address->getStreet(), + 'items_weight' => $address->getWeight(), + 'customer_notes' => $address->getCustomerNotes() + ]); + + if (!$address->hasItems()) { + return $addressData; + } + + $addressItemsData = []; + foreach ($address->getAllItems() as $addressItem) { + $addressItemsData[] = [ + 'cart_item_id' => $addressItem->getQuoteItemId(), + 'quantity' => $addressItem->getQty() + ]; + } + $addressData['cart_items'] = $addressItemsData; + + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php index c3207bf478bbe..3506ffc8c8792 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php @@ -13,6 +13,7 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; /** * Get cart @@ -29,28 +30,36 @@ class GetCartForUser */ private $cartRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId * @param CartRepositoryInterface $cartRepository + * @param StoreManagerInterface $storeManager */ public function __construct( MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + StoreManagerInterface $storeManager ) { $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; $this->cartRepository = $cartRepository; + $this->storeManager = $storeManager; } /** * Get cart for user * * @param string $cartHash - * @param int|null $userId + * @param int|null $customerId * @return Quote * @throws GraphQlAuthorizationException * @throws GraphQlNoSuchEntityException */ - public function execute(string $cartHash, ?int $userId): Quote + public function execute(string $cartHash, ?int $customerId): Quote { try { $cartId = $this->maskedQuoteIdToQuoteId->execute($cartHash); @@ -69,14 +78,29 @@ public function execute(string $cartHash, ?int $userId): Quote ); } - $customerId = (int)$cart->getCustomerId(); + if (false === (bool)$cart->getIsActive()) { + throw new GraphQlNoSuchEntityException( + __('Current user does not have an active cart.') + ); + } + + if ((int)$cart->getStoreId() !== (int)$this->storeManager->getStore()->getId()) { + throw new GraphQlNoSuchEntityException( + __( + 'Wrong store code specified for cart "%masked_cart_id"', + ['masked_cart_id' => $cartHash] + ) + ); + } + + $cartCustomerId = (int)$cart->getCustomerId(); /* Guest cart, allow operations */ - if (!$customerId) { + if (!$cartCustomerId && null === $customerId) { return $cart; } - if ($customerId !== $userId) { + if ($cartCustomerId !== $customerId) { throw new GraphQlAuthorizationException( __( 'The current user cannot perform operations on cart "%masked_cart_id"', diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.php new file mode 100644 index 0000000000000..89124c594dd87 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetQuoteAddress.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\ResourceModel\Quote\Address as AddressResource; + +/** + * Get quote address + */ +class GetQuoteAddress +{ + /** + * @var AddressInterfaceFactory + */ + private $quoteAddressFactory; + + /** + * @var AddressResource + */ + private $quoteAddressResource; + + /** + * @param AddressInterfaceFactory $quoteAddressFactory + * @param AddressResource $quoteAddressResource + */ + public function __construct( + AddressInterfaceFactory $quoteAddressFactory, + AddressResource $quoteAddressResource + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->quoteAddressResource = $quoteAddressResource; + } + + /** + * Get quote address + * + * @param CartInterface $cart + * @param int $quoteAddressId + * @param int|null $customerId + * @return AddressInterface + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(CartInterface $cart, int $quoteAddressId, ?int $customerId): AddressInterface + { + $quoteAddress = $this->quoteAddressFactory->create(); + + $this->quoteAddressResource->load($quoteAddress, $quoteAddressId); + if (null === $quoteAddress->getId()) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart address with ID "%cart_address_id"', ['cart_address_id' => $quoteAddressId]) + ); + } + + // TODO: GetQuoteAddress::execute should depend only on AddressInterface contract + // https://github.com/magento/graphql-ce/issues/550 + if ($quoteAddress->getQuoteId() !== $cart->getId()) { + throw new GraphQlNoSuchEntityException( + __('Cart does not contain address with ID "%cart_address_id"', ['cart_address_id' => $quoteAddressId]) + ); + } + + if ((int)$quoteAddress->getCustomerId() !== (int)$customerId) { + throw new GraphQlAuthorizationException( + __( + 'The current user cannot use cart address with ID "%cart_address_id"', + ['cart_address_id' => $quoteAddressId] + ) + ); + } + return $quoteAddress; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php new file mode 100644 index 0000000000000..13d6a1d3dce70 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Customer\Helper\Address as AddressHelper; +use Magento\CustomerGraphQl\Model\Customer\Address\GetCustomerAddress; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote\AddressFactory as BaseQuoteAddressFactory; + +/** + * Create QuoteAddress + */ +class QuoteAddressFactory +{ + /** + * @var BaseQuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var GetCustomerAddress + */ + private $getCustomerAddress; + + /** + * @var AddressHelper + */ + private $addressHelper; + + /** + * @param BaseQuoteAddressFactory $quoteAddressFactory + * @param GetCustomerAddress $getCustomerAddress + * @param AddressHelper $addressHelper + */ + public function __construct( + BaseQuoteAddressFactory $quoteAddressFactory, + GetCustomerAddress $getCustomerAddress, + AddressHelper $addressHelper + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->getCustomerAddress = $getCustomerAddress; + $this->addressHelper = $addressHelper; + } + + /** + * Create QuoteAddress based on input data + * + * @param array $addressInput + * @return QuoteAddress + * @throws GraphQlInputException + */ + public function createBasedOnInputData(array $addressInput): QuoteAddress + { + $addressInput['country_id'] = $addressInput['country_code'] ?? ''; + + $maxAllowedLineCount = $this->addressHelper->getStreetLines(); + if (is_array($addressInput['street']) && count($addressInput['street']) > $maxAllowedLineCount) { + throw new GraphQlInputException( + __('"Street Address" cannot contain more than %1 lines.', $maxAllowedLineCount) + ); + } + + $quoteAddress = $this->quoteAddressFactory->create(); + $quoteAddress->addData($addressInput); + return $quoteAddress; + } + + /** + * Create Quote Address based on Customer Address + * + * @param int $customerAddressId + * @param int $customerId + * @return QuoteAddress + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + */ + public function createBasedOnCustomerAddress(int $customerAddressId, int $customerId): QuoteAddress + { + $customerAddress = $this->getCustomerAddress->execute((int)$customerAddressId, $customerId); + + $quoteAddress = $this->quoteAddressFactory->create(); + try { + $quoteAddress->importCustomerAddressData($customerAddress); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + return $quoteAddress; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php new file mode 100644 index 0000000000000..c2bac13c07067 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set billing address for a specified shopping cart + */ +class SetBillingAddressOnCart +{ + /** + * @var QuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var AssignBillingAddressToCart + */ + private $assignBillingAddressToCart; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param GetCustomer $getCustomer + * @param AssignBillingAddressToCart $assignBillingAddressToCart + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + GetCustomer $getCustomer, + AssignBillingAddressToCart $assignBillingAddressToCart + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->getCustomer = $getCustomer; + $this->assignBillingAddressToCart = $assignBillingAddressToCart; + } + + /** + * Set billing address for a specified shopping cart + * + * @param ContextInterface $context + * @param CartInterface $cart + * @param array $billingAddressInput + * @return void + * @throws GraphQlInputException + * @throws GraphQlAuthenticationException + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, CartInterface $cart, array $billingAddressInput): void + { + $customerAddressId = $billingAddressInput['customer_address_id'] ?? null; + $addressInput = $billingAddressInput['address'] ?? null; + $useForShipping = isset($billingAddressInput['use_for_shipping']) + ? (bool)$billingAddressInput['use_for_shipping'] : false; + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The billing address must contain either "customer_address_id" or "address".') + ); + } + + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The billing address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + + $addresses = $cart->getAllShippingAddresses(); + if ($useForShipping && count($addresses) > 1) { + throw new GraphQlInputException( + __('Using the "use_for_shipping" option with multishipping is not possible.') + ); + } + + if (null === $customerAddressId) { + $billingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + } else { + $customer = $this->getCustomer->execute($context); + $billingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( + (int)$customerAddressId, + (int)$customer->getId() + ); + } + + $this->assignBillingAddressToCart->execute($cart, $billingAddress, $useForShipping); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php deleted file mode 100644 index b9fd5c7807d2f..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressOnCart.php +++ /dev/null @@ -1,98 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart; - -use Magento\Customer\Api\Data\AddressInterface; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote\Address; -use Magento\Quote\Model\ShippingAddressManagementInterface; -use Magento\Customer\Api\AddressRepositoryInterface; - -/** - * Set single shipping address for a specified shopping cart - */ -class SetShippingAddressOnCart implements SetShippingAddressesOnCartInterface -{ - /** - * @var ShippingAddressManagementInterface - */ - private $shippingAddressManagement; - - /** - * @var AddressRepositoryInterface - */ - private $addressRepository; - - /** - * @var Address - */ - private $addressModel; - - /** - * @var CheckCustomerAccount - */ - private $checkCustomerAccount; - - /** - * @param ShippingAddressManagementInterface $shippingAddressManagement - * @param AddressRepositoryInterface $addressRepository - * @param Address $addressModel - * @param CheckCustomerAccount $checkCustomerAccount - */ - public function __construct( - ShippingAddressManagementInterface $shippingAddressManagement, - AddressRepositoryInterface $addressRepository, - Address $addressModel, - CheckCustomerAccount $checkCustomerAccount - ) { - $this->shippingAddressManagement = $shippingAddressManagement; - $this->addressRepository = $addressRepository; - $this->addressModel = $addressModel; - $this->checkCustomerAccount = $checkCustomerAccount; - } - - /** - * @inheritdoc - */ - public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddresses): void - { - if (count($shippingAddresses) > 1) { - throw new GraphQlInputException( - __('You cannot specify multiple shipping addresses.') - ); - } - $shippingAddress = current($shippingAddresses); - $customerAddressId = $shippingAddress['customer_address_id'] ?? null; - $addressInput = $shippingAddress['address'] ?? null; - - if (null === $customerAddressId && null === $addressInput) { - throw new GraphQlInputException( - __('The shipping address must contain either "customer_address_id" or "address".') - ); - } - if ($customerAddressId && $addressInput) { - throw new GraphQlInputException( - __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') - ); - } - if (null === $customerAddressId) { - $shippingAddress = $this->addressModel->addData($addressInput); - } else { - $this->checkCustomerAccount->execute($context->getUserId(), $context->getUserType()); - - /** @var AddressInterface $customerAddress */ - $customerAddress = $this->addressRepository->getById($customerAddressId); - $shippingAddress = $this->addressModel->importCustomerAddressData($customerAddress); - } - - $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress); - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php new file mode 100644 index 0000000000000..6b0e2a311bf44 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set single shipping address for a specified shopping cart + */ +class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface +{ + /** + * @var QuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var AssignShippingAddressToCart + */ + private $assignShippingAddressToCart; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param GetCustomer $getCustomer + * @param AssignShippingAddressToCart $assignShippingAddressToCart + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + GetCustomer $getCustomer, + AssignShippingAddressToCart $assignShippingAddressToCart + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->getCustomer = $getCustomer; + $this->assignShippingAddressToCart = $assignShippingAddressToCart; + } + + /** + * @inheritdoc + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddressesInput): void + { + if (count($shippingAddressesInput) > 1) { + throw new GraphQlInputException( + __('You cannot specify multiple shipping addresses.') + ); + } + $shippingAddressInput = current($shippingAddressesInput); + $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; + $addressInput = $shippingAddressInput['address'] ?? null; + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The shipping address must contain either "customer_address_id" or "address".') + ); + } + + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + + if (null === $customerAddressId) { + $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + } else { + $customer = $this->getCustomer->execute($context); + $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( + (int)$customerAddressId, + (int)$customer->getId() + ); + } + + $this->assignShippingAddressToCart->execute($cart, $shippingAddress); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php index c5da3db75add7..eb0f3522102cf 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php @@ -7,7 +7,10 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Quote\Api\Data\CartInterface; @@ -24,9 +27,12 @@ interface SetShippingAddressesOnCartInterface * * @param ContextInterface $context * @param CartInterface $cart - * @param array $shippingAddresses + * @param array $shippingAddressesInput * @return void * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * @throws GraphQlAuthenticationException + * @throws GraphQlNoSuchEntityException */ - public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddresses): void; + public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddressesInput): void; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodOnCart.php deleted file mode 100644 index a630b2d07c7df..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodOnCart.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart; - -use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Exception\StateException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\AddressFactory as QuoteAddressFactory; -use Magento\Quote\Model\ResourceModel\Quote\Address as QuoteAddressResource; -use Magento\Checkout\Model\ShippingInformationFactory; -use Magento\Checkout\Api\ShippingInformationManagementInterface; -use Magento\Checkout\Model\ShippingInformation; - -/** - * Class SetShippingMethodsOnCart - * - * Set shipping method for a specified shopping cart address - */ -class SetShippingMethodOnCart -{ - /** - * @var ShippingInformationFactory - */ - private $shippingInformationFactory; - - /** - * @var QuoteAddressFactory - */ - private $quoteAddressFactory; - - /** - * @var QuoteAddressResource - */ - private $quoteAddressResource; - - /** - * @var ShippingInformationManagementInterface - */ - private $shippingInformationManagement; - - /** - * @param ShippingInformationManagementInterface $shippingInformationManagement - * @param QuoteAddressFactory $quoteAddressFactory - * @param QuoteAddressResource $quoteAddressResource - * @param ShippingInformationFactory $shippingInformationFactory - */ - public function __construct( - ShippingInformationManagementInterface $shippingInformationManagement, - QuoteAddressFactory $quoteAddressFactory, - QuoteAddressResource $quoteAddressResource, - ShippingInformationFactory $shippingInformationFactory - ) { - $this->shippingInformationManagement = $shippingInformationManagement; - $this->quoteAddressResource = $quoteAddressResource; - $this->quoteAddressFactory = $quoteAddressFactory; - $this->shippingInformationFactory = $shippingInformationFactory; - } - - /** - * Sets shipping method for a specified shopping cart address - * - * @param Quote $cart - * @param int $cartAddressId - * @param string $carrierCode - * @param string $methodCode - * @throws GraphQlInputException - * @throws GraphQlNoSuchEntityException - */ - public function execute(Quote $cart, int $cartAddressId, string $carrierCode, string $methodCode): void - { - $quoteAddress = $this->quoteAddressFactory->create(); - $this->quoteAddressResource->load($quoteAddress, $cartAddressId); - - /** @var ShippingInformation $shippingInformation */ - $shippingInformation = $this->shippingInformationFactory->create(); - - /* If the address is not a shipping address (but billing) the system will find the proper shipping address for - the selected cart and set the information there (actual for single shipping address) */ - $shippingInformation->setShippingAddress($quoteAddress); - $shippingInformation->setShippingCarrierCode($carrierCode); - $shippingInformation->setShippingMethodCode($methodCode); - - try { - $this->shippingInformationManagement->saveAddressInformation($cart->getId(), $shippingInformation); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__($exception->getMessage())); - } catch (StateException $exception) { - throw new GraphQlInputException(__($exception->getMessage())); - } catch (InputException $exception) { - throw new GraphQlInputException(__($exception->getMessage())); - } - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php new file mode 100644 index 0000000000000..730cf1b0ffee3 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set single shipping method for a specified shopping cart + */ +class SetShippingMethodsOnCart implements SetShippingMethodsOnCartInterface +{ + /** + * @var GetQuoteAddress + */ + private $getQuoteAddress; + + /** + * @var AssignShippingMethodToCart + */ + private $assignShippingMethodToCart; + + /** + * @param GetQuoteAddress $getQuoteAddress + * @param AssignShippingMethodToCart $assignShippingMethodToCart + */ + public function __construct( + GetQuoteAddress $getQuoteAddress, + AssignShippingMethodToCart $assignShippingMethodToCart + ) { + $this->getQuoteAddress = $getQuoteAddress; + $this->assignShippingMethodToCart = $assignShippingMethodToCart; + } + + /** + * @inheritdoc + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingMethodsInput): void + { + if (count($shippingMethodsInput) > 1) { + throw new GraphQlInputException( + __('You cannot specify multiple shipping methods.') + ); + } + $shippingMethodInput = current($shippingMethodsInput); + + if (!isset($shippingMethodInput['cart_address_id']) || empty($shippingMethodInput['cart_address_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_address_id" is missing.')); + } + $cartAddressId = $shippingMethodInput['cart_address_id']; + + if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) { + throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.')); + } + $carrierCode = $shippingMethodInput['carrier_code']; + + if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) { + throw new GraphQlInputException(__('Required parameter "method_code" is missing.')); + } + $methodCode = $shippingMethodInput['method_code']; + + $quoteAddress = $this->getQuoteAddress->execute($cart, $cartAddressId, $context->getUserId()); + $this->assignShippingMethodToCart->execute($cart, $quoteAddress, $carrierCode, $methodCode); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCartInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCartInterface.php new file mode 100644 index 0000000000000..fa6c6cf0923e4 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCartInterface.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Extension point for setting shipping methods for a specified shopping cart + * + * All objects that are responsible for setting shipping methods on a cart via GraphQl + * should implement this interface. + */ +interface SetShippingMethodsOnCartInterface +{ + /** + * Set shipping methods for a specified shopping cart + * + * @param ContextInterface $context + * @param CartInterface $cart + * @param array $shippingMethodsInput + * @return void + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * @throws GraphQlAuthenticationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingMethodsInput): void; +} 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 4b29eb6a4a663..96f11badac82e 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php @@ -42,6 +42,7 @@ public function getData( ): array { /** @var TextOptionType $optionTypeRenderer */ $optionTypeRenderer = $option->groupFactory($option->getType()); + $optionTypeRenderer->setOption($option); $priceValueUnits = $this->priceUnitLabel->getData($option->getPriceType()); $selectedOptionValueData = [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php index f4335b262c854..82ffd0970d672 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php @@ -11,9 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Stdlib\ArrayManager; use Magento\QuoteGraphQl\Model\Cart\AddProductsToCart; -use Magento\QuoteGraphQl\Model\Cart\ExtractDataFromCart; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; /** @@ -22,11 +20,6 @@ */ class AddSimpleProductsToCart implements ResolverInterface { - /** - * @var ArrayManager - */ - private $arrayManager; - /** * @var GetCartForUser */ @@ -38,26 +31,15 @@ class AddSimpleProductsToCart implements ResolverInterface private $addProductsToCart; /** - * @var ExtractDataFromCart - */ - private $extractDataFromCart; - - /** - * @param ArrayManager $arrayManager * @param GetCartForUser $getCartForUser * @param AddProductsToCart $addProductsToCart - * @param ExtractDataFromCart $extractDataFromCart */ public function __construct( - ArrayManager $arrayManager, GetCartForUser $getCartForUser, - AddProductsToCart $addProductsToCart, - ExtractDataFromCart $extractDataFromCart + AddProductsToCart $addProductsToCart ) { - $this->arrayManager = $arrayManager; $this->getCartForUser = $getCartForUser; $this->addProductsToCart = $addProductsToCart; - $this->extractDataFromCart = $extractDataFromCart; } /** @@ -65,25 +47,25 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - $cartHash = $this->arrayManager->get('input/cart_id', $args); - $cartItems = $this->arrayManager->get('input/cartItems', $args); - - if (!isset($cartHash)) { - throw new GraphQlInputException(__('Missing key "cart_id" in cart data')); + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } + $maskedCartId = $args['input']['cart_id']; - if (!isset($cartItems) || !is_array($cartItems) || empty($cartItems)) { - throw new GraphQlInputException(__('Missing key "cartItems" in cart data')); + if (!isset($args['input']['cartItems']) || empty($args['input']['cartItems']) + || !is_array($args['input']['cartItems']) + ) { + throw new GraphQlInputException(__('Required parameter "cartItems" is missing')); } + $cartItems = $args['input']['cartItems']; - $currentUserId = $context->getUserId(); - $cart = $this->getCartForUser->execute((string)$cartHash, $currentUserId); - + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); $this->addProductsToCart->execute($cart, $cartItems); - $cartData = $this->extractDataFromCart->execute($cart); return [ - 'cart' => $cartData, + 'cart' => [ + 'model' => $cart, + ], ]; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupon.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupon.php new file mode 100644 index 0000000000000..8251089abcd60 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupon.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CouponManagementInterface; + +/** + * @inheritdoc + */ +class AppliedCoupon implements ResolverInterface +{ + /** + * @var CouponManagementInterface + */ + private $couponManagement; + + /** + * @param CouponManagementInterface $couponManagement + */ + public function __construct( + CouponManagementInterface $couponManagement + ) { + $this->couponManagement = $couponManagement; + } + + /** + * @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')); + } + $cart = $value['model']; + $cartId = $cart->getId(); + + $appliedCoupon = $this->couponManagement->get($cartId); + return $appliedCoupon ? ['code' => $appliedCoupon] : null; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php index ec59416d49371..4de0464681186 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php @@ -50,12 +50,12 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($args['input']['cart_id'])) { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } $maskedCartId = $args['input']['cart_id']; - if (!isset($args['input']['coupon_code'])) { + if (!isset($args['input']['coupon_code']) || empty($args['input']['coupon_code'])) { throw new GraphQlInputException(__('Required parameter "coupon_code" is missing')); } $couponCode = $args['input']['coupon_code']; @@ -74,15 +74,20 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->couponManagement->set($cartId, $couponCode); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__($exception->getMessage())); - } catch (CouldNotSaveException $exception) { - throw new LocalizedException(__($exception->getMessage())); + } catch (NoSuchEntityException $e) { + $message = $e->getMessage(); + if (preg_match('/The "\d+" Cart doesn\'t contain products/', $message)) { + $message = 'Cart does not contain products.'; + } + throw new GraphQlNoSuchEntityException(__($message), $e); + } catch (CouldNotSaveException $e) { + throw new LocalizedException(__($e->getMessage()), $e); } - $data['cart']['applied_coupon'] = [ - 'code' => $couponCode, + return [ + 'cart' => [ + 'model' => $cart, + ], ]; - return $data; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php new file mode 100644 index 0000000000000..907d778550593 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Checkout\Api\PaymentInformationManagementInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Get list of active payment methods resolver. + */ +class AvailablePaymentMethods implements ResolverInterface +{ + /** + * @var PaymentInformationManagementInterface + */ + private $informationManagement; + + /** + * @param PaymentInformationManagementInterface $informationManagement + */ + public function __construct(PaymentInformationManagementInterface $informationManagement) + { + $this->informationManagement = $informationManagement; + } + + /** + * @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')); + } + + $cart = $value['model']; + return $this->getPaymentMethodsData($cart); + } + + /** + * Collect and return information about available payment methods + * + * @param CartInterface $cart + * @return array + */ + private function getPaymentMethodsData(CartInterface $cart): array + { + $paymentInformation = $this->informationManagement->getPaymentInformation($cart->getId()); + $paymentMethods = $paymentInformation->getPaymentMethods(); + + $paymentMethodsData = []; + foreach ($paymentMethods as $paymentMethod) { + $paymentMethodsData[] = [ + 'title' => $paymentMethod->getTitle(), + 'code' => $paymentMethod->getCode(), + ]; + } + return $paymentMethodsData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php new file mode 100644 index 0000000000000..a6bb0b0d04df1 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\CartInterface; +use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; + +/** + * @inheritdoc + */ +class BillingAddress implements ResolverInterface +{ + /** + * @var ExtractQuoteAddressData + */ + private $extractQuoteAddressData; + + /** + * @param ExtractQuoteAddressData $extractQuoteAddressData + */ + public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) + { + $this->extractQuoteAddressData = $extractQuoteAddressData; + } + + /** + * @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 CartInterface $cart */ + $cart = $value['model']; + + $billingAddress = $cart->getBillingAddress(); + if (null === $billingAddress) { + return null; + } + + $addressData = $this->extractQuoteAddressData->execute($billingAddress); + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php new file mode 100644 index 0000000000000..db74b1fa4174b --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class Cart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @param GetCartForUser $getCartForUser + */ + public function __construct( + GetCartForUser $getCartForUser + ) { + $this->getCartForUser = $getCartForUser; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['cart_id']) || empty($args['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['cart_id']; + + $currentUserId = $context->getUserId(); + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); + + return [ + 'model' => $cart, + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddresses.php deleted file mode 100644 index 69544672bf12e..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddresses.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Resolver; - -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\QuoteGraphQl\Model\Cart\Address\AddressDataProvider; - -/** - * @inheritdoc - */ -class CartAddresses implements ResolverInterface -{ - /** - * @var AddressDataProvider - */ - private $addressDataProvider; - - /** - * @param AddressDataProvider $addressDataProvider - */ - public function __construct( - AddressDataProvider $addressDataProvider - ) { - $this->addressDataProvider = $addressDataProvider; - } - - /** - * @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')); - } - - $cart = $value['model']; - - return $this->addressDataProvider->getCartAddresses($cart); - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartEmail.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartEmail.php new file mode 100644 index 0000000000000..8d0cb114d8315 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartEmail.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class CartEmail implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @param GetCartForUser $getCartForUser + */ + public function __construct( + GetCartForUser $getCartForUser + ) { + $this->getCartForUser = $getCartForUser; + } + + /** + * @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 Quote $cart */ + $cart = $value['model']; + + return $cart->getCustomerEmail(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php new file mode 100644 index 0000000000000..da6619d15a489 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote\Item as QuoteItem; + +/** + * @inheritdoc + */ +class CartItems implements ResolverInterface +{ + /** + * @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')); + } + $cart = $value['model']; + + $itemsData = []; + foreach ($cart->getAllItems() as $cartItem) { + /** + * @var QuoteItem $cartItem + */ + $productData = $cartItem->getProduct()->getData(); + $productData['model'] = $cartItem->getProduct(); + + $itemsData[] = [ + 'id' => $cartItem->getItemId(), + 'qty' => $cartItem->getQty(), + 'product' => $productData, + 'model' => $cartItem, + ]; + } + return $itemsData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php new file mode 100644 index 0000000000000..7a9bdd926764c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\Quote\Model\Quote\TotalsCollector; + +/** + * @inheritdoc + */ +class CartPrices implements ResolverInterface +{ + /** + * @var TotalsCollector + */ + private $totalsCollector; + + /** + * @param TotalsCollector $totalsCollector + */ + public function __construct( + TotalsCollector $totalsCollector + ) { + $this->totalsCollector = $totalsCollector; + } + + /** + * @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 Quote $quote */ + $quote = $value['model']; + $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); + $currency = $quote->getQuoteCurrencyCode(); + + return [ + 'grand_total' => ['value' => $cartTotals->getGrandTotal(), 'currency' => $currency], + 'subtotal_including_tax' => ['value' => $cartTotals->getSubtotalInclTax(), 'currency' => $currency], + 'subtotal_excluding_tax' => ['value' => $cartTotals->getSubtotal(), 'currency' => $currency], + 'subtotal_with_discount_excluding_tax' => [ + 'value' => $cartTotals->getSubtotalWithDiscount(), 'currency' => $currency + ], + 'applied_taxes' => $this->getAppliedTaxes($cartTotals, $currency), + 'model' => $quote + ]; + } + + /** + * Returns taxes applied to the current quote + * + * @param Total $total + * @param string $currency + * @return array + */ + private function getAppliedTaxes(Total $total, string $currency): array + { + $appliedTaxesData = []; + $appliedTaxes = $total->getAppliedTaxes(); + + if (count($appliedTaxes) === 0) { + return $appliedTaxesData; + } + + foreach ($appliedTaxes as $appliedTax) { + $appliedTaxesData[] = [ + 'label' => $appliedTax['id'], + 'amount' => ['value' => $appliedTax['amount'], 'currency' => $currency] + ]; + } + return $appliedTaxesData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php index 06123abe615e6..f020527d958e4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php @@ -7,13 +7,15 @@ namespace Magento\QuoteGraphQl\Model\Resolver; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Quote\Api\CartManagementInterface; -use Magento\Quote\Api\GuestCartManagementInterface; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; +use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForGuest; /** * @inheritdoc @@ -21,41 +23,33 @@ class CreateEmptyCart implements ResolverInterface { /** - * @var CartManagementInterface + * @var CreateEmptyCartForCustomer */ - private $cartManagement; + private $createEmptyCartForCustomer; /** - * @var GuestCartManagementInterface + * @var CreateEmptyCartForGuest */ - private $guestCartManagement; + private $createEmptyCartForGuest; /** - * @var QuoteIdToMaskedQuoteIdInterface + * @var MaskedQuoteIdToQuoteIdInterface */ - private $quoteIdToMaskedId; + private $maskedQuoteIdToQuoteId; /** - * @var QuoteIdMaskFactory - */ - private $quoteIdMaskFactory; - - /** - * @param CartManagementInterface $cartManagement - * @param GuestCartManagementInterface $guestCartManagement - * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId - * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer + * @param CreateEmptyCartForGuest $createEmptyCartForGuest + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId */ public function __construct( - CartManagementInterface $cartManagement, - GuestCartManagementInterface $guestCartManagement, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId, - QuoteIdMaskFactory $quoteIdMaskFactory + CreateEmptyCartForCustomer $createEmptyCartForCustomer, + CreateEmptyCartForGuest $createEmptyCartForGuest, + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId ) { - $this->cartManagement = $cartManagement; - $this->guestCartManagement = $guestCartManagement; - $this->quoteIdToMaskedId = $quoteIdToMaskedId; - $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; + $this->createEmptyCartForGuest = $createEmptyCartForGuest; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; } /** @@ -65,19 +59,49 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value { $customerId = $context->getUserId(); - if (0 !== $customerId && null !== $customerId) { - $quoteId = $this->cartManagement->createEmptyCartForCustomer($customerId); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quoteId); - - if (empty($maskedQuoteId)) { - $quoteIdMask = $this->quoteIdMaskFactory->create(); - $quoteIdMask->setQuoteId($quoteId)->save(); - $maskedQuoteId = $quoteIdMask->getMaskedId(); - } - } else { - $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); + $predefinedMaskedQuoteId = null; + if (isset($args['input']['cart_id'])) { + $predefinedMaskedQuoteId = $args['input']['cart_id']; + $this->validateMaskedId($predefinedMaskedQuoteId); } + $maskedQuoteId = (0 === $customerId || null === $customerId) + ? $this->createEmptyCartForGuest->execute($predefinedMaskedQuoteId) + : $this->createEmptyCartForCustomer->execute($customerId, $predefinedMaskedQuoteId); return $maskedQuoteId; } + + /** + * Validate masked id + * + * @param string $maskedId + * @throws GraphQlAlreadyExistsException + * @throws GraphQlInputException + */ + private function validateMaskedId(string $maskedId): void + { + if (mb_strlen($maskedId) != 32) { + throw new GraphQlInputException(__('Cart ID length should to be 32 symbols.')); + } + + if ($this->isQuoteWithSuchMaskedIdAlreadyExists($maskedId)) { + throw new GraphQlAlreadyExistsException(__('Cart with ID "%1" already exists.', $maskedId)); + } + } + + /** + * Check is quote with such maskedId already exists + * + * @param string $maskedId + * @return bool + */ + private function isQuoteWithSuchMaskedIdAlreadyExists(string $maskedId): bool + { + try { + $this->maskedQuoteIdToQuoteId->execute($maskedId); + return true; + } catch (NoSuchEntityException $e) { + return false; + } + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php new file mode 100644 index 0000000000000..1672474bb3ddd --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CartManagementInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * @inheritdoc + */ +class PlaceOrder implements ResolverInterface +{ + /** + * @var CartManagementInterface + */ + private $cartManagement; + + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @param GetCartForUser $getCartForUser + * @param CartManagementInterface $cartManagement + * @param OrderRepositoryInterface $orderRepository + */ + public function __construct( + GetCartForUser $getCartForUser, + CartManagementInterface $cartManagement, + OrderRepositoryInterface $orderRepository + ) { + $this->getCartForUser = $getCartForUser; + $this->cartManagement = $cartManagement; + $this->orderRepository = $orderRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + + if ($context->getUserId() === 0) { + if (!$cart->getCustomerEmail()) { + throw new GraphQlInputException(__("Guest email for cart is missing. Please enter")); + } + $cart->setCheckoutMethod(CartManagementInterface::METHOD_GUEST); + } + + try { + $orderId = $this->cartManagement->placeOrder($cart->getId()); + $order = $this->orderRepository->get($orderId); + + return [ + 'order' => [ + 'order_id' => $order->getIncrementId(), + ], + ]; + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Unable to place order: %message', ['message' => $e->getMessage()]), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php index c21d869ddac7d..f81ea3020d3d0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php @@ -50,7 +50,7 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($args['input']['cart_id'])) { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } $maskedCartId = $args['input']['cart_id']; @@ -61,15 +61,20 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value try { $this->couponManagement->remove($cartId); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__($exception->getMessage())); - } catch (CouldNotDeleteException $exception) { - throw new LocalizedException(__($exception->getMessage())); + } catch (NoSuchEntityException $e) { + $message = $e->getMessage(); + if (preg_match('/The "\d+" Cart doesn\'t contain products/', $message)) { + $message = 'Cart does not contain products'; + } + throw new GraphQlNoSuchEntityException(__($message), $e); + } catch (CouldNotDeleteException $e) { + throw new LocalizedException(__($e->getMessage()), $e); } - $data['cart']['applied_coupon'] = [ - 'code' => '', + return [ + 'cart' => [ + 'model' => $cart, + ], ]; - return $data; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php new file mode 100644 index 0000000000000..63e66f121f555 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class RemoveItemFromCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @param GetCartForUser $getCartForUser + * @param CartItemRepositoryInterface $cartItemRepository + */ + public function __construct( + GetCartForUser $getCartForUser, + CartItemRepositoryInterface $cartItemRepository + ) { + $this->getCartForUser = $getCartForUser; + $this->cartItemRepository = $cartItemRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['cart_item_id']) || empty($args['input']['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" is missing.')); + } + $itemId = $args['input']['cart_item_id']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + + try { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php new file mode 100644 index 0000000000000..7a99b04638ac3 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * @inheritdoc + */ +class SelectedPaymentMethod implements ResolverInterface +{ + /** + * @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 \Magento\Quote\Model\Quote $cart */ + $cart = $value['model']; + + $payment = $cart->getPayment(); + if (!$payment) { + return []; + } + + return [ + 'code' => $payment->getMethod(), + 'purchase_order_number' => $payment->getPoNumber(), + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php new file mode 100644 index 0000000000000..f7c9a4b0697b8 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\QuoteGraphQl\Model\Cart\SetBillingAddressOnCart as SetBillingAddressOnCartModel; + +/** + * Mutation resolver for setting billing address for shopping cart + */ +class SetBillingAddressOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var SetBillingAddressOnCartModel + */ + private $setBillingAddressOnCart; + + /** + * @param GetCartForUser $getCartForUser + * @param SetBillingAddressOnCartModel $setBillingAddressOnCart + */ + public function __construct( + GetCartForUser $getCartForUser, + SetBillingAddressOnCartModel $setBillingAddressOnCart + ) { + $this->getCartForUser = $getCartForUser; + $this->setBillingAddressOnCart = $setBillingAddressOnCart; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['billing_address']) || empty($args['input']['billing_address'])) { + throw new GraphQlInputException(__('Required parameter "billing_address" is missing')); + } + $billingAddress = $args['input']['billing_address']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->setBillingAddressOnCart->execute($context, $cart, $billingAddress); + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetGuestEmailOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetGuestEmailOnCart.php new file mode 100644 index 0000000000000..d621057348b54 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetGuestEmailOnCart.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class SetGuestEmailOnCart implements ResolverInterface +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var EmailAddressValidator + */ + private $emailValidator; + + /** + * @param GetCartForUser $getCartForUser + * @param CartRepositoryInterface $cartRepository + * @param EmailAddressValidator $emailValidator + */ + public function __construct( + GetCartForUser $getCartForUser, + CartRepositoryInterface $cartRepository, + EmailAddressValidator $emailValidator + ) { + $this->getCartForUser = $getCartForUser; + $this->cartRepository = $cartRepository; + $this->emailValidator = $emailValidator; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['email']) || empty($args['input']['email'])) { + throw new GraphQlInputException(__('Required parameter "email" is missing')); + } + + if (false === $this->emailValidator->isValid($args['input']['email'])) { + throw new GraphQlInputException(__('Invalid email format')); + } + $email = $args['input']['email']; + + $currentUserId = $context->getUserId(); + + if ($currentUserId !== 0) { + throw new GraphQlInputException(__('The request is not allowed for logged in customers')); + } + + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); + $cart->setCustomerEmail($email); + + try { + $this->cartRepository->save($cart); + } catch (CouldNotSaveException $e) { + throw new LocalizedException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php new file mode 100644 index 0000000000000..d1dcb4a48a76b --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Quote\Api\Data\PaymentInterfaceFactory; +use Magento\Quote\Api\PaymentMethodManagementInterface; + +/** + * Mutation resolver for setting payment method for shopping cart + */ +class SetPaymentMethodOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var PaymentMethodManagementInterface + */ + private $paymentMethodManagement; + + /** + * @var PaymentInterfaceFactory + */ + private $paymentFactory; + + /** + * @param GetCartForUser $getCartForUser + * @param PaymentMethodManagementInterface $paymentMethodManagement + * @param PaymentInterfaceFactory $paymentFactory + */ + public function __construct( + GetCartForUser $getCartForUser, + PaymentMethodManagementInterface $paymentMethodManagement, + PaymentInterfaceFactory $paymentFactory + ) { + $this->getCartForUser = $getCartForUser; + $this->paymentMethodManagement = $paymentMethodManagement; + $this->paymentFactory = $paymentFactory; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) { + throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); + } + $paymentMethodCode = $args['input']['payment_method']['code']; + + $poNumber = isset($args['input']['payment_method']['purchase_order_number']) + && empty($args['input']['payment_method']['purchase_order_number']) + ? $args['input']['payment_method']['purchase_order_number'] + : null; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $payment = $this->paymentFactory->create([ + 'data' => [ + PaymentInterface::KEY_METHOD => $paymentMethodCode, + PaymentInterface::KEY_PO_NUMBER => $poNumber, + PaymentInterface::KEY_ADDITIONAL_DATA => [], + ] + ]); + + try { + $this->paymentMethodManagement->set($cart->getId(), $payment); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php index b024e7b77af40..c3e1d371fe6a4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php @@ -11,62 +11,33 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Stdlib\ArrayManager; -use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; -use Magento\Quote\Model\ShippingAddressManagementInterface; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCartInterface; /** - * Class SetShippingAddressesOnCart - * * Mutation resolver for setting shipping addresses for shopping cart */ class SetShippingAddressesOnCart implements ResolverInterface { - /** - * @var MaskedQuoteIdToQuoteIdInterface - */ - private $maskedQuoteIdToQuoteId; - - /** - * @var ShippingAddressManagementInterface - */ - private $shippingAddressManagement; - /** * @var GetCartForUser */ private $getCartForUser; - /** - * @var ArrayManager - */ - private $arrayManager; - /** * @var SetShippingAddressesOnCartInterface */ private $setShippingAddressesOnCart; /** - * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId - * @param ShippingAddressManagementInterface $shippingAddressManagement * @param GetCartForUser $getCartForUser - * @param ArrayManager $arrayManager * @param SetShippingAddressesOnCartInterface $setShippingAddressesOnCart */ public function __construct( - MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, - ShippingAddressManagementInterface $shippingAddressManagement, GetCartForUser $getCartForUser, - ArrayManager $arrayManager, SetShippingAddressesOnCartInterface $setShippingAddressesOnCart ) { - $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; - $this->shippingAddressManagement = $shippingAddressManagement; $this->getCartForUser = $getCartForUser; - $this->arrayManager = $arrayManager; $this->setShippingAddressesOnCart = $setShippingAddressesOnCart; } @@ -75,26 +46,23 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - $shippingAddresses = $this->arrayManager->get('input/shipping_addresses', $args); - $maskedCartId = $this->arrayManager->get('input/cart_id', $args); - - if (!$maskedCartId) { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - if (!$shippingAddresses) { + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['shipping_addresses']) || empty($args['input']['shipping_addresses'])) { throw new GraphQlInputException(__('Required parameter "shipping_addresses" is missing')); } + $shippingAddresses = $args['input']['shipping_addresses']; - $maskedCartId = $args['input']['cart_id']; $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); - $this->setShippingAddressesOnCart->execute($context, $cart, $shippingAddresses); return [ 'cart' => [ - 'cart_id' => $maskedCartId, 'model' => $cart, - ] + ], ]; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php index 920829f5d67b1..e69ba47e7adf5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php @@ -11,45 +11,34 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Stdlib\ArrayManager; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\SetShippingMethodOnCart; +use Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCartInterface; /** - * Class SetShippingMethodsOnCart - * * Mutation resolver for setting shipping methods for shopping cart */ class SetShippingMethodsOnCart implements ResolverInterface { /** - * @var SetShippingMethodOnCart - */ - private $setShippingMethodOnCart; - - /** - * @var ArrayManager + * @var GetCartForUser */ - private $arrayManager; + private $getCartForUser; /** - * @var GetCartForUser + * @var SetShippingMethodsOnCartInterface */ - private $getCartForUser; + private $setShippingMethodsOnCart; /** - * @param ArrayManager $arrayManager * @param GetCartForUser $getCartForUser - * @param SetShippingMethodOnCart $setShippingMethodOnCart + * @param SetShippingMethodsOnCartInterface $setShippingMethodsOnCart */ public function __construct( - ArrayManager $arrayManager, GetCartForUser $getCartForUser, - SetShippingMethodOnCart $setShippingMethodOnCart + SetShippingMethodsOnCartInterface $setShippingMethodsOnCart ) { - $this->arrayManager = $arrayManager; $this->getCartForUser = $getCartForUser; - $this->setShippingMethodOnCart = $setShippingMethodOnCart; + $this->setShippingMethodsOnCart = $setShippingMethodsOnCart; } /** @@ -57,43 +46,23 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - $shippingMethods = $this->arrayManager->get('input/shipping_methods', $args); - $maskedCartId = $this->arrayManager->get('input/cart_id', $args); - - if (!$maskedCartId) { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - if (!$shippingMethods) { - throw new GraphQlInputException(__('Required parameter "shipping_methods" is missing')); - } + $maskedCartId = $args['input']['cart_id']; - $shippingMethod = reset($shippingMethods); // This point can be extended for multishipping - - if (!$shippingMethod['cart_address_id']) { - throw new GraphQlInputException(__('Required parameter "cart_address_id" is missing')); - } - if (!$shippingMethod['shipping_carrier_code']) { - throw new GraphQlInputException(__('Required parameter "shipping_carrier_code" is missing')); - } - if (!$shippingMethod['shipping_method_code']) { - throw new GraphQlInputException(__('Required parameter "shipping_method_code" is missing')); + if (!isset($args['input']['shipping_methods']) || empty($args['input']['shipping_methods'])) { + throw new GraphQlInputException(__('Required parameter "shipping_methods" is missing')); } + $shippingMethods = $args['input']['shipping_methods']; - $userId = $context->getUserId(); - $cart = $this->getCartForUser->execute((string) $maskedCartId, $userId); - - $this->setShippingMethodOnCart->execute( - $cart, - $shippingMethod['cart_address_id'], - $shippingMethod['shipping_carrier_code'], - $shippingMethod['shipping_method_code'] - ); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->setShippingMethodsOnCart->execute($context, $cart, $shippingMethods); return [ 'cart' => [ - 'cart_id' => $maskedCartId, - 'model' => $cart - ] + 'model' => $cart, + ], ]; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php new file mode 100644 index 0000000000000..a9e0ba59d15d9 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/AvailableShippingMethods.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver\ShippingAddress; + +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\ShippingMethodInterface; +use Magento\Quote\Model\Cart\ShippingMethodConverter; + +/** + * @inheritdoc + */ +class AvailableShippingMethods implements ResolverInterface +{ + /** + * @var ExtensibleDataObjectConverter + */ + private $dataObjectConverter; + + /** + * @var ShippingMethodConverter + */ + private $shippingMethodConverter; + + /** + * @param ExtensibleDataObjectConverter $dataObjectConverter + * @param ShippingMethodConverter $shippingMethodConverter + */ + public function __construct( + ExtensibleDataObjectConverter $dataObjectConverter, + ShippingMethodConverter $shippingMethodConverter + ) { + $this->dataObjectConverter = $dataObjectConverter; + $this->shippingMethodConverter = $shippingMethodConverter; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" values should be specified')); + } + $address = $value['model']; + + // Allow shipping rates by setting country id for new addresses + if (!$address->getCountryId() && $address->getCountryCode()) { + $address->setCountryId($address->getCountryCode()); + } + + $address->setCollectShippingRates(true); + $address->collectShippingRates(); + $cart = $address->getQuote(); + + $methods = []; + $shippingRates = $address->getGroupedAllShippingRates(); + foreach ($shippingRates as $carrierRates) { + foreach ($carrierRates as $rate) { + $methods[] = $this->dataObjectConverter->toFlatArray( + $this->shippingMethodConverter->modelToDataObject($rate, $cart->getQuoteCurrencyCode()), + [], + ShippingMethodInterface::class + ); + } + } + return $methods; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php new file mode 100644 index 0000000000000..c58affa064c89 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver\ShippingAddress; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * @inheritdoc + */ +class SelectedShippingMethod implements ResolverInterface +{ + /** + * @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')); + } + + $address = $value['model']; + + if ($address->getShippingMethod()) { + list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); + $shippingAmount = $address->getShippingAmount(); + } + + return [ + 'carrier_code' => $carrierCode ?? null, + 'method_code' => $methodCode ?? null, + 'label' => $address->getShippingDescription(), + 'amount' => $shippingAmount ?? null, + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php new file mode 100644 index 0000000000000..eb3b0966740eb --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; + +/** + * @inheritdoc + */ +class ShippingAddresses implements ResolverInterface +{ + /** + * @var ExtractQuoteAddressData + */ + private $extractQuoteAddressData; + + /** + * @param ExtractQuoteAddressData $extractQuoteAddressData + */ + public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) + { + $this->extractQuoteAddressData = $extractQuoteAddressData; + } + + /** + * @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 Quote $cart */ + $cart = $value['model']; + + $addressesData = []; + $shippingAddresses = $cart->getAllShippingAddresses(); + + if (count($shippingAddresses)) { + foreach ($shippingAddresses as $shippingAddress) { + $addressesData[] = $this->extractQuoteAddressData->execute($shippingAddress); + } + } + return $addressesData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php new file mode 100644 index 0000000000000..78a07506556c0 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class UpdateCartItems implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @param GetCartForUser $getCartForUser + * @param CartItemRepositoryInterface $cartItemRepository + */ + public function __construct( + GetCartForUser $getCartForUser, + CartItemRepositoryInterface $cartItemRepository + ) { + $this->getCartForUser = $getCartForUser; + $this->cartItemRepository = $cartItemRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['cart_items']) || empty($args['input']['cart_items']) + || !is_array($args['input']['cart_items']) + ) { + throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); + } + $cartItems = $args['input']['cart_items']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + + try { + $this->processCartItems($cart, $cartItems); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } + + /** + * Process cart items + * + * @param Quote $cart + * @param array $items + * @throws GraphQlInputException + * @throws LocalizedException + */ + private function processCartItems(Quote $cart, array $items): void + { + foreach ($items as $item) { + if (!isset($item['cart_item_id']) || empty($item['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); + } + $itemId = $item['cart_item_id']; + + if (!isset($item['quantity'])) { + throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); + } + $qty = (float)$item['quantity']; + + $cartItem = $cart->getItemById($itemId); + if ($cartItem === false) { + throw new GraphQlNoSuchEntityException( + __('Could not find cart item with id: %1.', $item['cart_item_id']) + ); + } + + if ($qty <= 0.0) { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } else { + $cartItem->setQty($qty); + $this->cartItemRepository->save($cartItem); + } + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 1bf4d581a5fe3..22ca9cfdfae9a 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -10,7 +10,8 @@ "magento/module-catalog": "*", "magento/module-store": "*", "magento/module-customer": "*", - "magento/module-customer-graph-ql": "*" + "magento/module-customer-graph-ql": "*", + "magento/module-sales": "*" }, "suggest": { "magento/module-graph-ql": "*" diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml index 63ad9e193b955..0697761a2a2a6 100644 --- a/app/code/Magento/QuoteGraphQl/etc/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -11,6 +11,8 @@ <arguments> <argument name="supportedTypes" xsi:type="array"> <item name="simple" xsi:type="string">SimpleCartItem</item> + <item name="virtual" xsi:type="string">VirtualCartItem</item> + <item name="configurable" xsi:type="string">ConfigurableCartItem</item> </argument> </arguments> </type> diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml index 86bc954ae4ac4..c7389cf667845 100644 --- a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml @@ -7,5 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCartInterface" - type="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressOnCart" /> + type="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCart"/> + <preference for="Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCartInterface" + type="Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCart"/> </config> diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index edc643973ce77..a9784e97c8952 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -2,30 +2,67 @@ # See COPYING.txt for license details. type Query { - getAvailableShippingMethodsOnCart(input: AvailableShippingMethodsOnCartInput): AvailableShippingMethodsOnCartOutput @doc(description:"Returns available shipping methods for cart by address/address_id") + cart(cart_id: String!): Cart @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart") @doc(description:"Returns information about shopping cart") } type Mutation { - createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user") - applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Coupon\\ApplyCouponToCart") - removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Coupon\\RemoveCouponFromCart") - setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingAddressesOnCart") + createEmptyCart(input: createEmptyCartInput): String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user") + addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") + addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart") removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveCouponFromCart") - setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput + updateCartItems(input: UpdateCartItemsInput): UpdateCartItemsOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\UpdateCartItems") + removeItemFromCart(input: RemoveItemFromCartInput): RemoveItemFromCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveItemFromCart") + setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingAddressesOnCart") + setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetBillingAddressOnCart") setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") - addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") + setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") + setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") + placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") } -input SetShippingAddressesOnCartInput { +input createEmptyCartInput { + cart_id: String +} + +input AddSimpleProductsToCartInput { cart_id: String! - shipping_addresses: [ShippingAddressInput!]! + cartItems: [SimpleProductCartItemInput!]! } -input ShippingAddressInput { - customer_address_id: Int # Can be provided in one-page checkout and is required for multi-shipping checkout - address: CartAddressInput - cart_items: [CartItemQuantityInput!] +input SimpleProductCartItemInput { + data: CartItemInput! + customizable_options:[CustomizableOptionInput!] +} + +input AddVirtualProductsToCartInput { + cart_id: String! + cartItems: [VirtualProductCartItemInput!]! +} + +input VirtualProductCartItemInput { + data: CartItemInput! + customizable_options:[CustomizableOptionInput!] +} + +input CartItemInput { + sku: String! + qty: Float! +} + +input CustomizableOptionInput { + id: Int! + value: String! +} + +input ApplyCouponToCartInput { + cart_id: String! + coupon_code: String! +} + +input UpdateCartItemsInput { + cart_id: String! + cart_items: [CartItemQuantityInput!]! } input CartItemQuantityInput { @@ -33,11 +70,30 @@ input CartItemQuantityInput { quantity: Float! } +input RemoveItemFromCartInput { + cart_id: String! + cart_item_id: Int! +} + +input SetShippingAddressesOnCartInput { + cart_id: String! + shipping_addresses: [ShippingAddressInput!]! +} + +input ShippingAddressInput { + customer_address_id: Int # If provided then will be used address from address book + address: CartAddressInput +} + input SetBillingAddressOnCartInput { cart_id: String! + billing_address: BillingAddressInput! +} + +input BillingAddressInput { customer_address_id: Int address: CartAddressInput - # TODO: consider adding "Same as shipping" option + use_for_shipping: Boolean } input CartAddressInput { @@ -55,56 +111,84 @@ input CartAddressInput { input SetShippingMethodsOnCartInput { cart_id: String! - shipping_methods: [ShippingMethodForAddressInput!]! + shipping_methods: [ShippingMethodInput!]! } -input ShippingMethodForAddressInput { +input ShippingMethodInput { cart_address_id: Int! - shipping_carrier_code: String! - shipping_method_code: String! + carrier_code: String! + method_code: String! } -type SetBillingAddressOnCartOutput { - cart: Cart! +input PlaceOrderInput { + cart_id: String! } -type SetShippingAddressesOnCartOutput { - cart: Cart! +input SetPaymentMethodOnCartInput { + cart_id: String! + payment_method: PaymentMethodInput! } -type SetShippingMethodsOnCartOutput { - cart: Cart! +input PaymentMethodInput { + code: String! @doc(description:"Payment method code") + purchase_order_number: String @doc(description:"Purchase order number") } -# If no address is provided, the system get address assigned to a quote -# If there's no address at all - the system returns all shipping methods -input AvailableShippingMethodsOnCartInput { +input SetGuestEmailOnCartInput { cart_id: String! - customer_address_id: Int - address: CartAddressInput + email: String! } -type AvailableShippingMethodsOnCartOutput { - available_shipping_methods: [CheckoutShippingMethod] +type CartPrices { + grand_total: Money + subtotal_including_tax: Money + subtotal_excluding_tax: Money + subtotal_with_discount_excluding_tax: Money + applied_taxes: [CartTaxItem] } -input ApplyCouponToCartInput { - cart_id: String! - coupon_code: String! +type CartTaxItem { + amount: Money! + label: String! +} + +type SetPaymentMethodOnCartOutput { + cart: Cart! +} + +type SetBillingAddressOnCartOutput { + cart: Cart! +} + +type SetShippingAddressesOnCartOutput { + cart: Cart! +} + +type SetShippingMethodsOnCartOutput { + cart: Cart! } type ApplyCouponToCartOutput { cart: Cart! } +type PlaceOrderOutput { + order: Order! +} + type Cart { - cart_id: String - items: [CartItemInterface] - applied_coupon: AppliedCoupon - addresses: [CartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddresses") + items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") + email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") + shipping_addresses: [CartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") + billing_address: CartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") + available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") + selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") + prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") } type CartAddress { + address_id: Int firstname: String lastname: String company: String @@ -115,15 +199,15 @@ type CartAddress { country: CartAddressCountry telephone: String address_type: AdressTypeEnum - selected_shipping_method: CheckoutShippingMethod - available_shipping_methods: [CheckoutShippingMethod] + available_shipping_methods: [AvailableShippingMethod] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\AvailableShippingMethods") + selected_shipping_method: SelectedShippingMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\SelectedShippingMethod") items_weight: Float customer_notes: String cart_items: [CartItemQuantity] } type CartItemQuantity { - cart_item_id: String! + cart_item_id: Int! quantity: Float! } @@ -137,12 +221,33 @@ type CartAddressCountry { label: String } -type CheckoutShippingMethod { - code: String +type SelectedShippingMethod { + carrier_code: String + method_code: String label: String - free_shipping: Boolean! + amount: Float +} + +type AvailableShippingMethod { + carrier_code: String! + carrier_title: String! + method_code: String! + method_title: String! error_message: String - # TODO: Add more complex structure for shipping rates + amount: Float! + base_amount: Float! + price_excl_tax: Float! + price_incl_tax: Float! +} + +type AvailablePaymentMethod { + code: String @doc(description: "The payment method code") + title: String @doc(description: "The payment method title.") +} + +type SelectedPaymentMethod { + code: String @doc(description: "The payment method code") + purchase_order_number: String @doc(description: "The purchase order number.") } enum AdressTypeEnum { @@ -162,22 +267,23 @@ type RemoveCouponFromCartOutput { cart: Cart } -input AddSimpleProductsToCartInput { - cart_id: String! - cartItems: [SimpleProductCartItemInput!]! +type AddSimpleProductsToCartOutput { + cart: Cart! } -input SimpleProductCartItemInput { - data: CartItemInput! - customizable_options:[CustomizableOptionInput!] +type AddVirtualProductsToCartOutput { + cart: Cart! } -input CustomizableOptionInput { - id: Int! - value: String! +type UpdateCartItemsOutput { + cart: Cart! } -type AddSimpleProductsToCartOutput { +type RemoveItemFromCartOutput { + cart: Cart! +} + +type SetGuestEmailOnCartOutput { cart: Cart! } @@ -185,9 +291,8 @@ type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") } -input CartItemInput { - sku: String! - qty: Float! +type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Cart Item") { + customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") } interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { @@ -218,3 +323,7 @@ type CartItemSelectedOptionValuePrice { units: String! type: PriceTypeEnum! } + +type Order { + order_id: String +} diff --git a/app/code/Magento/ReleaseNotification/etc/di.xml b/app/code/Magento/ReleaseNotification/etc/di.xml index 1404a6adb0a10..a4c434ff7f623 100644 --- a/app/code/Magento/ReleaseNotification/etc/di.xml +++ b/app/code/Magento/ReleaseNotification/etc/di.xml @@ -6,7 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> <preference for="Magento\ReleaseNotification\Model\ContentProviderInterface" type="Magento\ReleaseNotification\Model\ContentProvider\Http\HttpContentProvider" /> <type name="Magento\Config\Model\Config\TypePool"> <arguments> diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php index 9f5f784df677f..25a4aa1b88ca4 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php @@ -6,23 +6,58 @@ namespace Magento\Reports\Block\Adminhtml\Sales\Sales; +use Magento\Framework\DataObject; use Magento\Reports\Block\Adminhtml\Grid\Column\Renderer\Currency; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\Order\ConfigFactory; +use Magento\Sales\Model\Order; /** * Adminhtml sales report grid block * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Grid extends \Magento\Reports\Block\Adminhtml\Grid\AbstractGrid { /** - * GROUP BY criteria - * * @var string */ protected $_columnGroupBy = 'period'; + /** + * @var ConfigFactory + */ + private $configFactory; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Helper\Data $backendHelper + * @param \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory + * @param \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory + * @param \Magento\Reports\Helper\Data $reportsData + * @param array $data + * @param ConfigFactory|null $configFactory + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Helper\Data $backendHelper, + \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory, + \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory, + \Magento\Reports\Helper\Data $reportsData, + array $data = [], + ConfigFactory $configFactory = null + ) { + parent::__construct( + $context, + $backendHelper, + $resourceFactory, + $collectionFactory, + $reportsData, + $data + ); + $this->configFactory = $configFactory ?: ObjectManager::getInstance()->get(ConfigFactory::class); + } + /** * Reports grid constructor * @@ -331,4 +366,30 @@ protected function _prepareColumns() return parent::_prepareColumns(); } + + /** + * @inheritdoc + * + * Filter canceled statuses for orders. + * + * @return Grid + */ + protected function _prepareCollection() + { + /** @var DataObject $filterData */ + $filterData = $this->getData('filter_data'); + if (!$filterData->hasData('order_statuses')) { + $orderConfig = $this->configFactory->create(); + $statusValues = []; + $canceledStatuses = $orderConfig->getStateStatuses(Order::STATE_CANCELED); + $statusCodes = array_keys($orderConfig->getStatuses()); + foreach ($statusCodes as $code) { + if (!isset($canceledStatuses[$code])) { + $statusValues[] = $code; + } + } + $filterData->setData('order_statuses', $statusValues); + } + return parent::_prepareCollection(); + } } diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php index 6ba5b71b6c085..f4d2b962b9c9c 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php @@ -4,21 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Report statistics admin controller - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Reports\Controller\Adminhtml\Report; use Magento\Backend\Model\Auth\Session as AuthSession; use Magento\Backend\Model\Session; +use Magento\Framework\App\Action\HttpGetActionInterface; /** + * Report statistics admin controller. + * * @api * @since 100.0.2 */ -abstract class Statistics extends \Magento\Backend\App\Action +abstract class Statistics extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -49,7 +47,7 @@ abstract class Statistics extends \Magento\Backend\App\Action /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter - * @param [] $reportTypes + * @param array $reportTypes */ public function __construct( \Magento\Backend\App\Action\Context $context, diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php index 1b7ae6398d30e..b868394593558 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; -class RefreshLifetime extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Reports\Controller\Adminhtml\Report\Statistics; + +/** + * Refresh statistics action. + */ +class RefreshLifetime extends Statistics implements HttpPostActionInterface { /** * Refresh statistics for all period diff --git a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php index bc5ceda53481e..b6e55af96f4c1 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Customers Report collection - */ namespace Magento\Reports\Model\ResourceModel\Customer; /** + * Customers Report collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -74,6 +73,7 @@ class Collection extends \Magento\Customer\Model\ResourceModel\Customer\Collecti protected $orderResource; /** + * Collection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -88,7 +88,7 @@ class Collection extends \Magento\Customer\Model\ResourceModel\Customer\Collecti * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository * @param \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $quoteItemFactory * @param \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource - * @param mixed $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param string $modelName * * @SuppressWarnings(PHPMD.ExcessiveParameterList) diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 82ebc74a0468e..d89a118bff94b 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Reports\Model\ResourceModel\Order; use Magento\Framework\DB\Select; @@ -81,7 +80,7 @@ class Collection extends \Magento\Sales\Model\ResourceModel\Order\Collection * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Sales\Model\Order\Config $orderConfig * @param \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -446,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; @@ -770,11 +769,12 @@ public function addOrdersCount() */ public function addRevenueToSelect($convertCurrency = false) { - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( !$convertCurrency, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns(['revenue' => $expr]); @@ -792,11 +792,12 @@ public function addSumAvgTotals($storeId = 0) /** * calculate average and total amount */ - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( $storeId, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns( @@ -809,13 +810,15 @@ public function addSumAvgTotals($storeId = 0) } /** - * Get SQL expression for totals + * Get SQL expression for totals. * * @param int $storeId * @param string $baseSubtotalRefunded * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string + * @deprecated + * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( $storeId, @@ -825,11 +828,41 @@ protected function getTotalsExpression( ) { $template = ($storeId != 0) ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)' - : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) - %3$s) ' - . ' * main_table.base_to_global_rate)'; + : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) ' + . ' * main_table.base_to_global_rate)'; return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled); } + /** + * Get SQL expression for totals with discount refunded. + * + * @param int $storeId + * @param string $baseSubtotalRefunded + * @param string $baseSubtotalCanceled + * @param string $baseDiscountRefunded + * @param string $baseDiscountCanceled + * @return string + */ + private function getTotalsExpressionWithDiscountRefunded( + $storeId, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ) { + $template = ($storeId != 0) + ? '(main_table.base_subtotal - %2$s - %1$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s))' + : '((main_table.base_subtotal - %1$s - %2$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s)) ' + . ' * main_table.base_to_global_rate)'; + return sprintf( + $template, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ); + } + /** * Sort order by total amount * diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php index 337c87f6da03d..966ee14c2cb64 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php @@ -5,14 +5,15 @@ */ /** - * Products Report collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product; /** + * Products Report collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -88,7 +89,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Reports\Model\Event\TypeFactory $eventTypeFactory * @param \Magento\Catalog\Model\Product\Type $productType * @param \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteResource - * @param mixed $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -149,7 +150,8 @@ public function __construct( } /** - * Set Type for COUNT SQL Select + * Set Type for COUNT SQL Select. + * * @codeCoverageIgnore * * @param int $type @@ -162,7 +164,8 @@ public function setSelectCountSqlType($type) } /** - * Set product entity id + * Set product entity id. + * * @codeCoverageIgnore * * @param string $entityId @@ -175,7 +178,8 @@ public function setProductEntityId($entityId) } /** - * Get product entity id + * Get product entity id. + * * @codeCoverageIgnore * * @return int @@ -186,7 +190,8 @@ public function getProductEntityId() } /** - * Set product entity table name + * Set product entity table name. + * * @codeCoverageIgnore * * @param string $value @@ -199,7 +204,8 @@ public function setProductEntityTableName($value) } /** - * Get product entity table name + * Get product entity table name. + * * @codeCoverageIgnore * * @return string @@ -210,7 +216,8 @@ public function getProductEntityTableName() } /** - * Get product attribute set id + * Get product attribute set id. + * * @codeCoverageIgnore * * @return int @@ -221,7 +228,8 @@ public function getProductAttributeSetId() } /** - * Set product attribute set id + * Set product attribute set id. + * * @codeCoverageIgnore * * @param int $value diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php index 1985db0b90e2a..2009cd3ff9d92 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ +namespace Magento\Reports\Model\ResourceModel\Product\Downloads; + /** * Product Downloads Report collection * * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\Reports\Model\ResourceModel\Product\Downloads; - -/** + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -97,4 +97,14 @@ public function addFieldToFilter($field, $condition = null) } return $this; } + + /** + * @inheritDoc + */ + public function getSelectCountSql() + { + $countSelect = parent::getSelectCountSql(); + $countSelect->reset(\Zend\Db\Sql\Select::GROUP); + return $countSelect; + } } diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php index 7371bc4359f46..5b4cf39d65def 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php @@ -5,14 +5,16 @@ */ /** - * Reports Product Index Abstract Product Resource Collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product\Index\Collection; /** + * Reports Product Index Abstract Product Resource Collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -52,7 +54,7 @@ abstract class AbstractCollection extends \Magento\Catalog\Model\ResourceModel\P * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param \Magento\Customer\Model\Visitor $customerVisitor - * @param mixed $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -181,7 +183,8 @@ protected function _getWhereCondition() } /** - * Set customer id, that will be used in 'whereCondition' + * Set customer id, that will be used in 'whereCondition'. + * * @codeCoverageIgnore * * @param int $id diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php index 732d819e3b2cd..39d673911111f 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php @@ -14,7 +14,10 @@ use Magento\Framework\Exception\LocalizedException; /** + * Product Low Stock Report Collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -50,7 +53,6 @@ class Collection extends \Magento\Reports\Model\ResourceModel\Product\Collection protected $_itemResource; /** - * Collection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy diff --git a/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php b/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php index bbe431aeeef9c..26559dc27cc53 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php @@ -24,6 +24,7 @@ class CatalogProductCompareClearObserver implements ObserverInterface /** * @param \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory, @@ -39,7 +40,7 @@ public function __construct( * Reset count of compared products cache * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute(\Magento\Framework\Event\Observer $observer) diff --git a/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php b/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php index 7797dda8eabfb..b3ec141ef01a7 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php @@ -10,6 +10,7 @@ /** * Reports Event observer model + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class CatalogProductViewObserver implements ObserverInterface { @@ -49,6 +50,7 @@ class CatalogProductViewObserver implements ObserverInterface * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Visitor $customerVisitor * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, diff --git a/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php b/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php index 718cc02349ce5..6a3b7832bd48a 100644 --- a/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php @@ -25,6 +25,7 @@ class CheckoutCartAddProductObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php b/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php index 833834d06bc74..95d17ddacefb3 100644 --- a/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php +++ b/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php @@ -38,7 +38,7 @@ public function __construct( * Customer logout processing * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute(\Magento\Framework\Event\Observer $observer) diff --git a/app/code/Magento/Reports/Observer/SendfriendProductObserver.php b/app/code/Magento/Reports/Observer/SendfriendProductObserver.php index 0583b45d2d05f..ca70b23d55ee2 100644 --- a/app/code/Magento/Reports/Observer/SendfriendProductObserver.php +++ b/app/code/Magento/Reports/Observer/SendfriendProductObserver.php @@ -25,6 +25,7 @@ class SendfriendProductObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php b/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php index 3fd868abbd968..e4c57cf3ef25a 100644 --- a/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php @@ -25,6 +25,7 @@ class WishlistAddProductObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Observer/WishlistShareObserver.php b/app/code/Magento/Reports/Observer/WishlistShareObserver.php index 2c4926ac12a16..de6e55ceb3f6c 100644 --- a/app/code/Magento/Reports/Observer/WishlistShareObserver.php +++ b/app/code/Magento/Reports/Observer/WishlistShareObserver.php @@ -25,6 +25,7 @@ class WishlistShareObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml new file mode 100644 index 0000000000000..d367b2deb5922 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.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="GenerateOrderReportActionGroup"> + <arguments> + <argument name="orderFromDate" type="string"/> + <argument name="orderToDate" type="string"/> + </arguments> + <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere" /> + <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> + <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Any" stepKey="selectAnyOption" /> + <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport" /> + </actionGroup> + <actionGroup name="GenerateOrderReportForNotCancelActionGroup"> + <arguments> + <argument name="orderFromDate" type="string"/> + <argument name="orderToDate" type="string"/> + <argument name="statuses" type="string"/> + </arguments> + <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere" /> + <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> + <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Specified" stepKey="selectSpecifiedOption" /> + <selectOption selector="{{OrderReportFilterSection.orderStatusSpecified}}" parameterArray="{{statuses}}" stepKey="selectSpecifiedOptionStatus" /> + <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Reports/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..e77e3ee8abd87 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,86 @@ +<?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="AdminMenuReportsMarketingAbandonedCarts"> + <data key="pageTitle">Abandoned Carts</data> + <data key="title">Abandoned Carts</data> + <data key="dataUiId">magento-reports-report-shopcart-abandoned</data> + </entity> + <entity name="AdminMenuReportsProductsBestsellers"> + <data key="pageTitle">Bestsellers Report</data> + <data key="title">Bestsellers</data> + <data key="dataUiId">magento-reports-report-products-bestsellers</data> + </entity> + <entity name="AdminMenuReportsSalesCoupons"> + <data key="pageTitle">Coupons Report</data> + <data key="title">Coupons</data> + <data key="dataUiId">magento-reports-report-salesroot-coupons</data> + </entity> + <entity name="AdminMenuReportsProductsDownloads"> + <data key="pageTitle">Downloads Report</data> + <data key="title">Downloads</data> + <data key="dataUiId">magento-downloadable-report-products-downloads</data> + </entity> + <entity name="AdminMenuReportsSalesInvoiced"> + <data key="pageTitle">Invoice Report</data> + <data key="title">Invoiced</data> + <data key="dataUiId">magento-reports-report-salesroot-invoiced</data> + </entity> + <entity name="AdminMenuReportsProductsLowStock"> + <data key="pageTitle">Low Stock Report</data> + <data key="title">Low Stock</data> + <data key="dataUiId">magento-reports-report-products-lowstock</data> + </entity> + <entity name="AdminMenuReportsCustomersNew"> + <data key="pageTitle">New Accounts Report</data> + <data key="title">New</data> + <data key="dataUiId">magento-reports-report-customers-accounts</data> + </entity> + <entity name="AdminMenuReportsCustomersOrderCount"> + <data key="pageTitle">Order Count Report</data> + <data key="title">Order Count</data> + <data key="dataUiId">magento-reports-report-customers-orders</data> + </entity> + <entity name="AdminMenuReportsProductsOrdered"> + <data key="pageTitle">Ordered Products Report</data> + <data key="title">Ordered</data> + <data key="dataUiId">magento-reports-report-products-sold</data> + </entity> + <entity name="AdminMenuReportsSalesOrders"> + <data key="pageTitle">Orders Report</data> + <data key="title">Orders</data> + <data key="dataUiId">magento-reports-report-salesroot-sales</data> + </entity> + <entity name="AdminMenuReportsCustomersOrderTotal"> + <data key="pageTitle">Order Total Report</data> + <data key="title">Order Total</data> + <data key="dataUiId">magento-reports-report-customers-totals</data> + </entity> + <entity name="AdminMenuReportsMarketingProductsInCarts"> + <data key="pageTitle">Products in Carts</data> + <data key="title">Products in Cart</data> + <data key="dataUiId">magento-reports-report-shopcart-product</data> + </entity> + <entity name="AdminMenuReportsStatisticsRefreshStatistics"> + <data key="pageTitle">Refresh Statistics</data> + <data key="title">Refresh Statistics</data> + <data key="dataUiId">magento-reports-report-statistics-refresh</data> + </entity> + <entity name="AdminMenuReportsSalesTax"> + <data key="pageTitle">Tax Report</data> + <data key="title">Tax</data> + <data key="dataUiId">magento-reports-report-salesroot-tax</data> + </entity> + <entity name="AdminMenuReportsProductsViews"> + <data key="pageTitle">Product Views Report</data> + <data key="title">Views</data> + <data key="dataUiId">magento-reports-report-products-viewed</data> + </entity> +</entities> diff --git a/app/code/Magento/Reports/Test/Mftf/Page/OrdersReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/OrdersReportPage.xml new file mode 100644 index 0000000000000..46509089b97ba --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/OrdersReportPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="OrdersReportPage" url="reports/report_sales/sales/" area="admin" module="Reports"> + <section name="OrderReportFilterSection"/> + <section name="OrderReportMainSection"/> + <section name="GeneratedReportSection" /> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml new file mode 100644 index 0000000000000..7ad9bdfa8c12c --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml @@ -0,0 +1,29 @@ +<?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="OrderReportMainSection"> + <element name="showReport" type="button" selector="#filter_form_submit"/> + <element name="here" type="text" selector="//a[contains(text(), 'here')]"/> + </section> + + <section name="OrderReportFilterSection"> + <element name="dateFrom" type="input" selector="#sales_report_from"/> + <element name="dateTo" type="input" selector="#sales_report_to"/> + <element name="orderStatus" type="select" selector="#sales_report_show_order_statuses"/> + <element name="optionAny" type="option" selector="//select[@id='sales_report_show_order_statuses']/option[contains(text(), 'Any')]"/> + <element name="optionSpecified" type="option" selector="//select[@id='sales_report_show_order_statuses']/option[contains(text(), 'Specified')]"/> + <element name="orderStatusSpecified" type="select" selector="#sales_report_order_statuses"/> + </section> + + <section name="GeneratedReportSection"> + <element name="ordersCount" type="text" selector="//tr[@class='totals']/th[@class=' col-orders col-orders_count col-number']"/> + <element name="canceledOrders" type="text" selector="//tr[@class='totals']/th[@class=' col-canceled col-total_canceled_amount a-right']"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml new file mode 100644 index 0000000000000..342955e0684b3 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsAbandonedCartsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports abandoned carts navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Abandoned Carts"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14159"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAbandonedCartsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingAbandonedCarts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingAbandonedCarts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml new file mode 100644 index 0000000000000..259f2cde2786a --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsBestsellersNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports bestsellers navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Bestsellers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14168"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsBestsellersPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsBestsellers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsBestsellers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml new file mode 100644 index 0000000000000..321f3078bc63f --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsCouponsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports coupons navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Coupons"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14163"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsCouponsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesCoupons.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesCoupons.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml new file mode 100644 index 0000000000000..584c1af6683aa --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsDownloadsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports downloads navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Downloads"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14171"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsDownloadsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsDownloads.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsDownloads.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml new file mode 100644 index 0000000000000..34aec0620cad9 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsInvoicedNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports invoiced navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Invoiced"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14162"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsInvoicedPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesInvoiced.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesInvoiced.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml new file mode 100644 index 0000000000000..5d91d65a3a457 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsLowStockNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports low stock navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Low Stock"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14169"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsLowStockPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsLowStock.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsLowStock.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml new file mode 100644 index 0000000000000..aeb35ba65a380 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsNewNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports new navigate menu test"/> + <description value="Admin should be able to navigate to Reports > New"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14166"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsNewPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsCustomersNew.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsCustomersNew.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml new file mode 100644 index 0000000000000..1bfbc654746e6 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsOrderCountNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports order count navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Order Count"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14165"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrderCountPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsCustomersOrderCount.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsCustomersOrderCount.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml new file mode 100644 index 0000000000000..88c94b53f5233 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsOrderTotalNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports order total navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Order Total"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14164"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrderTotalPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsCustomersOrderTotal.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsCustomersOrderTotal.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml new file mode 100644 index 0000000000000..e81239539a5b5 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsOrderedNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports ordered navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Ordered"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14170"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrderedPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsOrdered.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsOrdered.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml new file mode 100644 index 0000000000000..13fc8e7353139 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsOrdersNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports orders navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Orders"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14160"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrdersPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesOrders.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesOrders.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml new file mode 100644 index 0000000000000..03877f8e58ecc --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsProductsInCartNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports products in cart navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Products in Cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14158"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsProductsInCartPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingProductsInCarts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingProductsInCarts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml new file mode 100644 index 0000000000000..d05fc091357df --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsRefreshStatisticsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports refresh statistics navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Refresh Statistics"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14172"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsRefreshStatisticsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsStatisticsRefreshStatistics.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsStatisticsRefreshStatistics.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml new file mode 100644 index 0000000000000..11a065c933a3b --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsTaxNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports tax navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Tax"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14161"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsTaxPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesTax.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesTax.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml new file mode 100644 index 0000000000000..9154b96c71e38 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsViewsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports views navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Views"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14167"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsViewsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsViews.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsViews.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml new file mode 100644 index 0000000000000..7cb23e54aa1b7 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CancelOrdersInOrderSalesReportTest"> + <annotations> + <features value="Reports"/> + <stories value="Order Sales Report includes canceled orders"/> + <group value="reports"/> + <title value="Canceled orders in order sales report"/> + <description value="Verify canceling of orders in order sales report"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95960"/> + <useCaseId value="MAGETWO-95823"/> + </annotations> + + <before> + <!-- log in as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- create new product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- create new customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + + <!-- Create completed order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrderd"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + + <!-- Create Order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Cancel order --> + <actionGroup ref="cancelPendingOrder" stepKey="cancelOrder"/> + + <!-- Generate Order report for statuses --> + <amOnPage url="{{OrdersReportPage.url}}" stepKey="goToOrdersReportPage1"/> + <!-- Get date --> + <generateDate stepKey="generateEndDate" date="+0 day" format="m/d/Y"/> + <generateDate stepKey="generateStartDate" date="-1 day" format="m/d/Y"/> + <actionGroup ref="GenerateOrderReportForNotCancelActionGroup" stepKey="generateReportAfterCancelOrderBefore"> + <argument name="orderFromDate" value="$generateStartDate"/> + <argument name="orderToDate" value="$generateEndDate"/> + <argument name="statuses" value="['closed', 'complete', 'fraud', 'holded', 'payment_review', 'paypal_canceled_reversal', 'paypal_reversed', 'processing']"/> + </actionGroup> + <waitForElement selector="{{GeneratedReportSection.ordersCount}}" stepKey="waitForOrdersCountBefore"/> + <grabTextFrom selector="{{GeneratedReportSection.ordersCount}}" stepKey="grabCanceledOrdersSpecified"/> + <!-- Generate Order report --> + <amOnPage url="{{OrdersReportPage.url}}" stepKey="goToOrdersReportPage2"/> + <!-- Get date --> + <actionGroup ref="GenerateOrderReportActionGroup" stepKey="generateReportAfterCancelOrder"> + <argument name="orderFromDate" value="$generateStartDate"/> + <argument name="orderToDate" value="$generateEndDate"/> + </actionGroup> + <waitForElement selector="{{GeneratedReportSection.ordersCount}}" stepKey="waitForOrdersCount"/> + <grabTextFrom selector="{{GeneratedReportSection.ordersCount}}" stepKey="grabCanceledOrdersAny"/> + + <!-- Compare canceled orders price --> + <assertEquals expected="{$grabCanceledOrdersSpecified}" expectedType="string" actual="{$grabCanceledOrdersAny}" actualType="string" stepKey="assertEquals"/> + </test> +</tests> diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 96dd02e65f18a..c5600fe061003 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -56,7 +56,7 @@ protected function _construct() }, loadProductData : function() { jQuery.ajax({ - type: "POST", + type: "GET", url: review.productInfoUrl, data: { form_key: FORM_KEY diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index d6868eae6fcbc..f6f0ccef9b4e7 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -159,13 +159,13 @@ protected function _construct() } if ($this->getRequest()->getParam('ret', false) == 'pending') { - $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')'); + $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')'); $this->buttonList->update( 'delete', 'onclick', 'deleteConfirm(' . '\'' . __( 'Are you sure you want to do this?' - ) . '\' ' . '\'' . $this->getUrl( + ) . '\', ' . '\'' . $this->getUrl( '*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending'] ) . '\'' . ')' diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php b/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php index 8a8395de72b62..4f7237a0b44be 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ +namespace Magento\Review\Block\Adminhtml\Edit; + /** * Adminhtml Review Edit Form */ -namespace Magento\Review\Block\Adminhtml\Edit; - class Form extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -84,7 +84,8 @@ protected function _prepareForm() 'review/*/save', [ 'id' => $this->getRequest()->getParam('id'), - 'ret' => $this->_coreRegistry->registry('ret') + 'ret' => $this->_coreRegistry->registry('ret'), + 'productId' => $this->getRequest()->getParam('productId') ] ), 'method' => 'post', diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 35187e46933bc..57b1e538ddb6b 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -10,9 +10,14 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; +/** + * Save Review action. + */ class Save extends ProductController implements HttpPostActionInterface { /** + * Save Review action. + * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -64,10 +69,14 @@ public function execute() if ($nextId) { $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } + $productId = (int)$this->getRequest()->getParam('productId'); + if ($productId) { + $resultRedirect->setPath("catalog/product/edit/id/$productId"); + } return $resultRedirect; } $resultRedirect->setPath('review/*/'); diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index 3033a31ff1723..ab264ef1b6179 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -5,16 +5,17 @@ */ namespace Magento\Review\Model\ResourceModel\Review\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; /** * Review Product Collection * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection diff --git a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php index 6256194cef53b..f35d6eac27ea8 100644 --- a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php +++ b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Review block observer. + */ class CatalogBlockProductCollectionBeforeToHtmlObserver implements ObserverInterface { /** @@ -35,7 +38,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) { $productCollection = $observer->getEvent()->getCollection(); if ($productCollection instanceof \Magento\Framework\Data\Collection) { - $productCollection->load(); + if (!$productCollection->isLoaded()) { + $productCollection->load(); + } $this->_reviewFactory->create()->appendSummary($productCollection); } diff --git a/app/code/Magento/Review/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Review/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..89882707f5ebd --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?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="AdminMenuUserContentReviews"> + <data key="pageTitle">Reviews</data> + <data key="title">Reviews</data> + <data key="dataUiId">magento-review-catalog-reviews-ratings-reviews-all</data> + </entity> + <entity name="AdminMenuReportsReviewsByCustomers"> + <data key="pageTitle">Customer Reviews Report</data> + <data key="title">By Customers</data> + <data key="dataUiId">magento-review-report-review-customer</data> + </entity> + <entity name="AdminMenuReportsReviewsByProducts"> + <data key="pageTitle">Product Reviews Report</data> + <data key="title">By Products</data> + <data key="dataUiId">magento-review-report-review-product</data> + </entity> + <entity name="AdminMenuAttributesRating"> + <data key="pageTitle">Ratings</data> + <data key="title">Rating</data> + <data key="dataUiId">magento-review-catalog-reviews-ratings-ratings</data> + </entity> +</entities> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml new file mode 100644 index 0000000000000..fade220d22100 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingReviewsNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing reviews navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Reviews"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14196"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsViewsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuUserContentReviews.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuUserContentReviews.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml new file mode 100644 index 0000000000000..58492424e76f7 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsByCustomersNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin reports by customers navigate menu test"/> + <description value="Admin should be able to navigate to Reports > By Customers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14197"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsByCustomersPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsReviewsByCustomers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsReviewsByCustomers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml new file mode 100644 index 0000000000000..e848aa4f22023 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminReportsByProductsNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin reports by products navigate menu test"/> + <description value="Admin should be able to navigate to Reports > By Products"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14198"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsByProductsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsReviewsByProducts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsReviewsByProducts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml new file mode 100644 index 0000000000000..511ed5439dc3d --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresRatingNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin stores rating navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Rating"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14199"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresRatingPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuAttributesRating.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuAttributesRating.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d6..46fdb20dee4a1 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -16,8 +16,8 @@ </resource> <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> - <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::reviews_all" title="All Reviews" translate="title" sortOrder="10"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index e3532483f88af..7376329471921 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -8,7 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> - <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="20" action="review/product/pending" resource="Magento_Review::pending"/> + <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="All Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml index 6fcf5b0c82b4f..a6b46f8f25a71 100644 --- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml @@ -19,6 +19,9 @@ </referenceContainer> <referenceBlock name="product.info.details"> <block class="Magento\Review\Block\Product\Review" name="reviews.tab" as="reviews" template="Magento_Review::review.phtml" group="detailed_info" ifconfig="catalog/review/active"> + <arguments> + <argument name="sort_order" xsi:type="string">30</argument> + </arguments> <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before"/> </block> diff --git a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js index d1c40959e3ec2..88c61fa38af34 100644 --- a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js +++ b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js @@ -20,7 +20,7 @@ define([ showLoader: false, loaderContext: $('.product.data.items') }).done(function (data) { - $('#product-review-container').html(data); + $('#product-review-container').html(data).trigger('contentUpdated'); $('[data-role="product-review"] .pages a').each(function (index, element) { $(element).click(function (event) { //eslint-disable-line max-nested-callbacks processReviews($(element).attr('href'), true); diff --git a/app/code/Magento/ReviewAnalytics/composer.json b/app/code/Magento/ReviewAnalytics/composer.json index 73f534451580c..a82d4328ca159 100644 --- a/app/code/Magento/ReviewAnalytics/composer.json +++ b/app/code/Magento/ReviewAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-review": "*" + "magento/module-review": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index c4e17e55f1262..16a5a486e1078 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Robots\Model\Config; use Magento\Framework\App\Cache\TypeListInterface; @@ -32,12 +33,11 @@ class Value extends ConfigValue implements IdentityInterface const CACHE_TAG = 'robots'; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [self::CACHE_TAG]; /** * @var StoreManagerInterface diff --git a/app/code/Magento/Rule/Block/Editable.php b/app/code/Magento/Rule/Block/Editable.php index 67e4671236ea0..d53213a7df876 100644 --- a/app/code/Magento/Rule/Block/Editable.php +++ b/app/code/Magento/Rule/Block/Editable.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Element\AbstractBlock; /** + * Renderer for Editable sales rules + * * @api * @since 100.0.2 */ @@ -52,9 +54,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele if ($element->getShowAsText()) { $html = ' <input type="hidden" class="hidden" id="' . - $element->getHtmlId() . + $this->escapeHtmlAttr($element->getHtmlId()) . '" name="' . - $element->getName() . + $this->escapeHtmlAttr($element->getName()) . '" value="' . $element->getValue() . '" data-form-part="' . diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index a1987f67e47f2..6729fe722de56 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -62,7 +62,8 @@ abstract class AbstractCondition extends \Magento\Framework\DataObject implement protected $_layout; /** - * Base name for hidden elements + * Base name for hidden elements. + * * @var string */ protected $elementName = 'rule'; @@ -105,8 +106,8 @@ public function getDefaultOperatorInputByType() 'string' => ['==', '!=', '>=', '>', '<=', '<', '{}', '!{}', '()', '!()'], 'numeric' => ['==', '!=', '>=', '>', '<=', '<', '()', '!()'], 'date' => ['==', '>=', '<='], - 'select' => ['==', '!='], - 'boolean' => ['==', '!='], + 'select' => ['==', '!=', '<=>'], + 'boolean' => ['==', '!=', '<=>'], 'multiselect' => ['{}', '!{}', '()', '!()'], 'grid' => ['()', '!()'], ]; @@ -116,8 +117,9 @@ public function getDefaultOperatorInputByType() } /** - * Default operator options getter - * Provides all possible operator options + * Default operator options getter. + * + * Provides all possible operator options. * * @return array */ @@ -135,12 +137,15 @@ public function getDefaultOperatorOptions() '!{}' => __('does not contain'), '()' => __('is one of'), '!()' => __('is not one of'), + '<=>' => __('is undefined'), ]; } return $this->_defaultOperatorOptions; } /** + * Get rule form. + * * @return Form */ public function getForm() @@ -149,6 +154,8 @@ public function getForm() } /** + * Get condition as array. + * * @param array $arrAttributes * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -195,6 +202,8 @@ public function getMappedSqlField() } /** + * Get condition as xml. + * * @return string */ public function asXml() @@ -214,6 +223,8 @@ public function asXml() } /** + * Load condition from array. + * * @param array $arr * @return $this * @SuppressWarnings(PHPMD.NPathComplexity) @@ -229,6 +240,8 @@ public function loadArray($arr) } /** + * Load condition from xml. + * * @param string|array $xml * @return $this */ @@ -242,6 +255,8 @@ public function loadXml($xml) } /** + * Load attribute options. + * * @return $this */ public function loadAttributeOptions() @@ -250,6 +265,8 @@ public function loadAttributeOptions() } /** + * Get attribute options. + * * @return array */ public function getAttributeOptions() @@ -258,6 +275,8 @@ public function getAttributeOptions() } /** + * Get attribute select options. + * * @return array */ public function getAttributeSelectOptions() @@ -270,6 +289,8 @@ public function getAttributeSelectOptions() } /** + * Get attribute name. + * * @return string */ public function getAttributeName() @@ -278,6 +299,8 @@ public function getAttributeName() } /** + * Load operator options. + * * @return $this */ public function loadOperatorOptions() @@ -300,6 +323,8 @@ public function getInputType() } /** + * Get operator select options. + * * @return array */ public function getOperatorSelectOptions() @@ -316,6 +341,8 @@ public function getOperatorSelectOptions() } /** + * Get operator name. + * * @return array */ public function getOperatorName() @@ -324,6 +351,8 @@ public function getOperatorName() } /** + * Load value options. + * * @return $this */ public function loadValueOptions() @@ -333,6 +362,8 @@ public function loadValueOptions() } /** + * Get value select options. + * * @return array */ public function getValueSelectOptions() @@ -380,6 +411,8 @@ public function isArrayOperatorType() } /** + * Get value. + * * @return mixed */ public function getValue() @@ -395,6 +428,8 @@ public function getValue() } /** + * Get value name. + * * @return array|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -446,6 +481,8 @@ public function getNewChildSelectOptions() } /** + * Get new child name. + * * @return string */ public function getNewChildName() @@ -454,6 +491,8 @@ public function getNewChildName() } /** + * Get this condition as html. + * * @return string */ public function asHtml() @@ -467,6 +506,8 @@ public function asHtml() } /** + * Get this condition with subconditions as html. + * * @return string */ public function asHtmlRecursive() @@ -475,6 +516,8 @@ public function asHtmlRecursive() } /** + * Get type element. + * * @return AbstractElement */ public function getTypeElement() @@ -493,6 +536,8 @@ public function getTypeElement() } /** + * Get type element html. + * * @return string */ public function getTypeElementHtml() @@ -501,6 +546,8 @@ public function getTypeElementHtml() } /** + * Get attribute element. + * * @return $this */ public function getAttributeElement() @@ -528,6 +575,8 @@ public function getAttributeElement() } /** + * Get attribute element html. + * * @return string */ public function getAttributeElementHtml() @@ -536,8 +585,9 @@ public function getAttributeElementHtml() } /** - * Retrieve Condition Operator element Instance - * If the operator value is empty - define first available operator value as default + * Retrieve Condition Operator element Instance. + * + * If the operator value is empty - define first available operator value as default. * * @return \Magento\Framework\Data\Form\Element\Select */ @@ -568,6 +618,8 @@ public function getOperatorElement() } /** + * Get operator element html. + * * @return string */ public function getOperatorElementHtml() @@ -587,6 +639,8 @@ public function getValueElementType() } /** + * Get value element renderer. + * * @return \Magento\Rule\Block\Editable */ public function getValueElementRenderer() @@ -598,6 +652,8 @@ public function getValueElementRenderer() } /** + * Get value element. + * * @return $this */ public function getValueElement() @@ -615,6 +671,9 @@ public function getValueElement() // date format intentionally hard-coded $elementParams['input_format'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; $elementParams['date_format'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; + $elementParams['placeholder'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; + $elementParams['autocomplete'] = 'off'; + $elementParams['readonly'] = 'true'; } return $this->getForm()->addField( $this->getPrefix() . '__' . $this->getId() . '__value', @@ -626,6 +685,8 @@ public function getValueElement() } /** + * Get value element html. + * * @return string */ public function getValueElementHtml() @@ -634,6 +695,8 @@ public function getValueElementHtml() } /** + * Get add link html. + * * @return string */ public function getAddLinkHtml() @@ -643,6 +706,8 @@ public function getAddLinkHtml() } /** + * Get remove link html. + * * @return string */ public function getRemoveLinkHtml() @@ -655,6 +720,8 @@ public function getRemoveLinkHtml() } /** + * Get chooser container html. + * * @return string */ public function getChooserContainerHtml() @@ -664,6 +731,8 @@ public function getChooserContainerHtml() } /** + * Get this condition as string. + * * @param string $format * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -674,6 +743,8 @@ public function asString($format = '') } /** + * Get this condition with subconditions as string. + * * @param int $level * @return string */ @@ -816,6 +887,8 @@ protected function _compareValues($validatedValue, $value, $strict = true) } /** + * Validate model. + * * @param \Magento\Framework\Model\AbstractModel $model * @return bool */ diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index 5ab1379b96cf6..e216e2ae658ba 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -95,8 +95,8 @@ abstract class AbstractProduct extends \Magento\Rule\Model\Condition\AbstractCon * @param \Magento\Catalog\Model\ResourceModel\Product $productResource * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection * @param \Magento\Framework\Locale\FormatInterface $localeFormat - * @param ProductCategoryList|null $categoryList * @param array $data + * @param ProductCategoryList|null $categoryList * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -514,6 +514,10 @@ public function loadArray($arr) ) ? $this->_localeFormat->getNumber( $arr['is_value_parsed'] ) : false; + } elseif (!empty($arr['operator']) && $arr['operator'] == '()') { + if (isset($arr['value'])) { + $arr['value'] = preg_replace('/\s*,\s*/', ',', $arr['value']); + } } return parent::loadArray($arr); @@ -695,6 +699,7 @@ protected function _getAttributeSetId($productId) /** * Correct '==' and '!=' operators + * * Categories can't be equal because product is included categories selected by administrator and in their parents * * @return string diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 6267e30a7a6d5..33e1bf97c3474 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -250,8 +250,30 @@ public function attachConditionToCollection( $this->_joinTablesToCollection($collection, $combine); $whereExpression = (string)$this->_getMappedSqlCombination($combine); if (!empty($whereExpression)) { - // Select ::where method adds braces even on empty expression - $collection->getSelect()->where($whereExpression); + if (!empty($combine->getConditions())) { + $conditions = ''; + $attributeField = ''; + foreach ($combine->getConditions() as $condition) { + if ($condition->getData('attribute') === \Magento\Catalog\Api\Data\ProductInterface::SKU) { + $conditions = $condition->getData('value'); + $attributeField = $condition->getMappedSqlField(); + } + } + + $collection->getSelect()->where($whereExpression); + + if (!empty($conditions) && !empty($attributeField)) { + $conditions = explode(',', $conditions); + foreach ($conditions as &$condition) { + $condition = "'" . trim($condition) . "'"; + } + $conditions = implode(', ', $conditions); + $collection->getSelect()->order("FIELD($attributeField, $conditions)"); + } + } else { + // Select ::where method adds braces even on empty expression + $collection->getSelect()->where($whereExpression); + } } } } diff --git a/app/code/Magento/Rule/view/adminhtml/web/rules.js b/app/code/Magento/Rule/view/adminhtml/web/rules.js index b094b9818364a..95175d272f9af 100644 --- a/app/code/Magento/Rule/view/adminhtml/web/rules.js +++ b/app/code/Magento/Rule/view/adminhtml/web/rules.js @@ -13,6 +13,7 @@ define([ 'mage/translate', 'prototype' ], function (jQuery) { + 'use strict'; var VarienRulesForm = new Class.create(); @@ -101,6 +102,9 @@ define([ if (!elem.multiple) { Event.observe(elem, 'change', this.hideParamInputField.bind(this, container)); + + this.changeVisibilityForValueRuleParam(elem); + } Event.observe(elem, 'blur', this.hideParamInputField.bind(this, container)); } @@ -122,7 +126,7 @@ define([ var values = this.updateElement.value.split(','), s = ''; - for (i = 0; i < values.length; i++) { + for (var i = 0; i < values.length; i++) { s = values[i].strip(); if (s != '') { @@ -220,6 +224,8 @@ define([ var elem = Element.down(elemContainer, 'input.input-text'); + jQuery(elem).trigger('contentUpdated'); + if (elem) { elem.focus(); @@ -249,7 +255,7 @@ define([ if (elem && elem.options) { var selectedOptions = []; - for (i = 0; i < elem.options.length; i++) { + for (var i = 0; i < elem.options.length; i++) { if (elem.options[i].selected) { selectedOptions.push(elem.options[i].text); } @@ -260,6 +266,8 @@ define([ label.innerHTML = str != '' ? str : '...'; } + this.changeVisibilityForValueRuleParam(elem); + elem = Element.down(container, 'input.input-text'); if (elem) { @@ -291,6 +299,23 @@ define([ this.shownElement = null; }, + changeVisibilityForValueRuleParam: function(elem) { + var parsedElementId = elem.id.split('__'); + if (parsedElementId[2] !== 'operator') { + return false; + } + + var valueElement = jQuery('#' + parsedElementId[0] + '__' + parsedElementId[1] + '__value'); + + if(elem.value === '<=>') { + valueElement.closest('.rule-param').hide(); + } else { + valueElement.closest('.rule-param').show(); + } + + return true; + }, + addRuleNewChild: function (elem) { var parent_id = elem.id.replace(/^.*__(.*)__.*$/, '$1'); var children_ul_id = elem.id.replace(/__/g, ':').replace(/[^:]*$/, 'children').replace(/:/g, '__'); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php index d15c218a60b47..6b87c1fe39d8b 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; /** * Sales Order Create Form Abstract Block @@ -57,8 +58,7 @@ public function __construct( } /** - * Prepare global layout - * Add renderers to \Magento\Framework\Data\Form + * Prepare global layout. Add renderers to \Magento\Framework\Data\Form * * @return $this */ @@ -152,7 +152,7 @@ protected function _addAdditionalFormElementData(\Magento\Framework\Data\Form\El /** * Add rendering EAV attributes to Form element * - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface[] $attributes + * @param AttributeMetadataInterface[] $attributes * @param \Magento\Framework\Data\Form\AbstractForm $form * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -176,8 +176,8 @@ protected function _addAttributesToForm($attributes, \Magento\Framework\Data\For [ 'name' => $attribute->getAttributeCode(), 'label' => __($attribute->getStoreLabel()), - 'class' => $attribute->getFrontendClass(), - 'required' => $attribute->isRequired() + 'class' => $this->getValidationClasses($attribute), + 'required' => $attribute->isRequired(), ] ); if ($inputType == 'multiline') { @@ -227,4 +227,58 @@ public function getFormValues() { return []; } + + /** + * Retrieve frontend classes according validation rules + * + * @param AttributeMetadataInterface $attribute + * + * @return string + */ + private function getValidationClasses(AttributeMetadataInterface $attribute) : string + { + $out = []; + $out[] = $attribute->getFrontendClass(); + + $textClasses = $this->getTextLengthValidateClasses($attribute); + if (!empty($textClasses)) { + $out = array_merge($out, $textClasses); + } + + $out = !empty($out) ? implode(' ', array_unique(array_filter($out))) : ''; + return $out; + } + + /** + * Retrieve validation classes by min_text_length and max_text_length rules + * + * @param AttributeMetadataInterface $attribute + * + * @return array + */ + private function getTextLengthValidateClasses(AttributeMetadataInterface $attribute) : array + { + $classes = []; + + $validateRules = $attribute->getValidationRules(); + if (!empty($validateRules)) { + foreach ($validateRules as $rule) { + switch ($rule->getName()) { + case 'min_text_length': + $classes[] = 'minimum-length-' . $rule->getValue(); + break; + + case 'max_text_length': + $classes[] = 'maximum-length-' . $rule->getValue(); + break; + } + } + + if (!empty($classes)) { + $classes[] = 'validate-length'; + } + } + + return $classes; + } } 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 26379a05fe694..03915c0499367 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 @@ -132,15 +132,7 @@ protected function _prepareForm() $this->_addAttributesToForm($attributes, $fieldset); $this->_form->addFieldNameSuffix('order[account]'); - - $formValues = $this->getFormValues(); - foreach ($attributes as $code => $attribute) { - $defaultValue = $attribute->getDefaultValue(); - if (isset($defaultValue) && !isset($formValues[$code])) { - $formValues[$code] = $defaultValue; - } - } - $this->_form->setValues($formValues); + $this->_form->setValues($this->extractValuesFromAttributes($attributes)); return $this; } @@ -193,4 +185,23 @@ public function getFormValues() return $data; } + + /** + * Extract the form values from attributes. + * + * @param array $attributes + * @return array + */ + private function extractValuesFromAttributes(array $attributes): array + { + $formValues = $this->getFormValues(); + foreach ($attributes as $code => $attribute) { + $defaultValue = $attribute->getDefaultValue(); + if (isset($defaultValue) && !isset($formValues[$code])) { + $formValues[$code] = $defaultValue; + } + } + + return $formValues; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php index f21cc96be92bc..7cb46fcde2c48 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php @@ -29,7 +29,7 @@ class Load extends \Magento\Framework\View\Element\Template /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder - * @param \Magento\Framework\View\Helper\Js $adminhtmlJs + * @param \Magento\Framework\View\Helper\Js $jsHelper * @param array $data */ public function __construct( diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 4bd2227d4bb1e..9a271f741edda 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,12 +5,17 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection + as ProductCollectionDataProvider; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml sales order create search products block * * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -42,6 +47,11 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended */ protected $_productFactory; + /** + * @var ProductCollectionDataProvider $productCollectionProvider + */ + private $productCollectionProvider; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper @@ -50,6 +60,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data + * @param ProductCollectionDataProvider|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -58,12 +69,15 @@ public function __construct( \Magento\Catalog\Model\Config $catalogConfig, \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, - array $data = [] + array $data = [], + ProductCollectionDataProvider $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; + $this->productCollectionProvider = $productCollectionProvider + ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); parent::__construct($context, $backendHelper, $data); } @@ -140,20 +154,18 @@ protected function _addColumnFilterToCollection($column) */ protected function _prepareCollection() { + $attributes = $this->_catalogConfig->getProductAttributes(); + $store = $this->getStore(); + /* @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ - $collection = $this->_productFactory->create()->getCollection(); - $collection->setStore( - $this->getStore() - )->addAttributeToSelect( + $collection = $this->productCollectionProvider->getCollectionForStore($store); + $collection->addAttributeToSelect( $attributes - )->addAttributeToSelect( - 'sku' - )->addStoreFilter()->addAttributeToFilter( + ); + $collection->addAttributeToFilter( 'type_id', $this->_salesConfig->getAvailableProductTypes() - )->addAttributeToSelect( - 'gift_message_available' ); $this->setCollection($collection); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php new file mode 100644 index 0000000000000..733791a2f9549 --- /dev/null +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider; + +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Store\Model\Store; + +/** + * Prepares product collection for the grid + */ +class ProductCollection +{ + /** + * @var ProductCollectionFactory + */ + private $collectionFactory; + + /** + * @param ProductCollectionFactory $collectionFactory + */ + public function __construct( + ProductCollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Provide products collection filtered with store + * + * @param Store $store + * @return Collection + */ + public function getCollectionForStore(Store $store):Collection + { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + $collection->setStore($store); + $collection->addAttributeToSelect( + 'gift_message_available' + ); + $collection->addAttributeToSelect( + 'sku' + ); + $collection->addStoreFilter(); + + return $collection; + } +} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index 34d7a3f8ee25e..f2200e1c1a108 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Pricing\Price\FinalPrice; + /** * Adminhtml sales order create sidebar cart block * @@ -58,6 +63,17 @@ public function getItemCollection() return $collection; } + /** + * @inheritdoc + */ + public function getItemPrice(Product $product) + { + $customPrice = $this->getCartItemCustomPrice($product); + $price = $customPrice ?? $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); + + return $this->convertPrice($price); + } + /** * Retrieve display item qty availability * @@ -111,4 +127,23 @@ protected function _prepareLayout() return parent::_prepareLayout(); } + + /** + * Returns cart item custom price. + * + * @param Product $product + * @return float|null + */ + private function getCartItemCustomPrice(Product $product): ?float + { + $items = $this->getItemCollection(); + foreach ($items as $item) { + $productItemId = $this->getProduct($item)->getId(); + if ($productItemId === $product->getId() && $item->getCustomPrice()) { + return (float)$item->getCustomPrice(); + } + } + + return null; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index d73371d46dae1..9e13e9424d1fd 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -8,6 +8,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Credit memo adjustmets block + * * @api * @since 100.0.2 */ @@ -50,7 +52,7 @@ public function __construct( } /** - * Initialize creditmemo agjustment totals + * Initialize creditmemo adjustment totals * * @return $this */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php index d2e42fe388da7..ec959fc286333 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php @@ -26,7 +26,7 @@ public function getOrder() /** * Retrieve source * - * @return \Magento\Sales\Model\Order\Invoice + * @return \Magento\Sales\Model\Order\Creditmemo */ public function getSource() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php index fd6e5f403f2de..074aa99a5e791 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php @@ -113,8 +113,8 @@ protected function _construct() $orderPayment->canRefund() && !$this->getInvoice()->getIsUsedForRefund() ) { $this->buttonList->add( - 'capture', - [ // capture? + 'credit-memo', + [ 'label' => __('Credit Memo'), 'class' => 'credit-memo', 'onclick' => 'setLocation(\'' . $this->getCreditMemoUrl() . '\')' diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php index 04a9f9437ae57..d19fc4992f046 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php @@ -94,6 +94,7 @@ public function getFieldIdPrefix() * Indicate that block can display container * * @return bool + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function canDisplayContainer() { @@ -196,7 +197,7 @@ public function getMessage() /** * Retrieve save url * - * @return array + * @return string */ public function getSaveUrl() { @@ -259,9 +260,11 @@ public function displayPriceInclTax(\Magento\Framework\DataObject $item) } /** + * Retrieve rendered column html content + * * @param \Magento\Framework\DataObject|Item $item * @param string $column - * @param null $field + * @param string $field * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @since 100.1.0 @@ -301,6 +304,8 @@ public function getColumnHtml(\Magento\Framework\DataObject $item, $column, $fie } /** + * Get columns data. + * * @return array * @since 100.1.0 */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php index 512539824da20..802ed1dc60f30 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php @@ -7,6 +7,7 @@ /** * Class Link + * * @package Magento\Sales\Block\Adminhtml\Rss\Order\Grid */ class Link extends \Magento\Framework\View\Element\Template @@ -36,6 +37,8 @@ public function __construct( } /** + * Get url for link. + * * @return string */ public function getLink() @@ -44,6 +47,8 @@ public function getLink() } /** + * Get translatable label for link. + * * @return \Magento\Framework\Phrase */ public function getLabel() @@ -62,7 +67,9 @@ public function isRssAllowed() } /** - * @return string + * Get link type param. + * + * @return array */ protected function getLinkParams() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Totals.php index 83b155293c2b9..8172a3c0db4ad 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Totals.php @@ -5,6 +5,11 @@ */ namespace Magento\Sales\Block\Adminhtml; +use Magento\Sales\Model\Order; + +/** + * Adminhtml sales totals block + */ class Totals extends \Magento\Sales\Block\Order\Totals { /** @@ -67,12 +72,16 @@ protected function _initTotals() if (!$this->getSource()->getIsVirtual() && ((double)$this->getSource()->getShippingAmount() || $this->getSource()->getShippingDescription()) ) { + $shippingLabel = __('Shipping & Handling'); + if ($this->isFreeShipping($this->getOrder()) && $this->getSource()->getDiscountDescription()) { + $shippingLabel .= sprintf(' (%s)', $this->getSource()->getDiscountDescription()); + } $this->_totals['shipping'] = new \Magento\Framework\DataObject( [ 'code' => 'shipping', 'value' => $this->getSource()->getShippingAmount(), 'base_value' => $this->getSource()->getBaseShippingAmount(), - 'label' => __('Shipping & Handling'), + 'label' => $shippingLabel, ] ); } @@ -109,4 +118,23 @@ protected function _initTotals() return $this; } + + /** + * Availability of free shipping in at least one order item + * + * @param Order $order + * @return bool + */ + private function isFreeShipping(Order $order): bool + { + $isFreeShipping = false; + foreach ($order->getItems() as $orderItem) { + if ($orderItem->getFreeShipping() == '1') { + $isFreeShipping = true; + break; + } + } + + return $isFreeShipping; + } } diff --git a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php index a5785a19cc66a..bc7756816d32a 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php @@ -4,14 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Sales Order Email Invoice items - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Sales\Block\Order\Email\Invoice; /** + * Sales Order Email Invoice items + * * @api * @since 100.0.2 */ @@ -21,7 +18,7 @@ class Items extends \Magento\Sales\Block\Items\AbstractItems * Prepare item before output * * @param \Magento\Framework\View\Element\AbstractBlock $renderer - * @return \Magento\Sales\Block\Items\AbstractItems + * @return void */ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $renderer) { diff --git a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php index 21c83e55a489d..a4c9a7b80a00d 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php @@ -4,14 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Sales Order Email Shipment items - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Sales\Block\Order\Email\Shipment; /** + * Sales Order Email Shipment items + * * @api * @since 100.0.2 */ @@ -21,7 +18,7 @@ class Items extends \Magento\Sales\Block\Items\AbstractItems * Prepare item before output * * @param \Magento\Framework\View\Element\AbstractBlock $renderer - * @return \Magento\Sales\Block\Items\AbstractItems + * @return void */ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $renderer) { diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php index 2b84b8f1444b6..626dcf2a5a474 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php @@ -46,6 +46,8 @@ public function __construct( } /** + * Get link url. + * * @return string */ public function getLink() @@ -54,6 +56,8 @@ public function getLink() } /** + * Get translatable label for url. + * * @return \Magento\Framework\Phrase */ public function getLabel() @@ -91,7 +95,10 @@ protected function getUrlKey($order) } /** - * @return string + * Get type, secure and query params for link. + * + * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function getLinkParams() { diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index 0946492711748..83e66bbbce7cc 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -48,6 +48,8 @@ public function __construct( } /** + * Set item. + * * @param \Magento\Framework\DataObject $item * @return $this */ @@ -58,6 +60,8 @@ public function setItem(\Magento\Framework\DataObject $item) } /** + * Get item. + * * @return array|null */ public function getItem() @@ -76,6 +80,8 @@ public function getOrder() } /** + * Get order item. + * * @return array|null */ public function getOrderItem() @@ -88,6 +94,8 @@ public function getOrderItem() } /** + * Get item options. + * * @return array */ public function getItemOptions() diff --git a/app/code/Magento/Sales/Block/Order/PrintShipment.php b/app/code/Magento/Sales/Block/Order/PrintShipment.php index 335b6095d0ca4..0006a38f0f1ce 100644 --- a/app/code/Magento/Sales/Block/Order/PrintShipment.php +++ b/app/code/Magento/Sales/Block/Order/PrintShipment.php @@ -53,6 +53,8 @@ public function __construct( } /** + * Preparing global layout. + * * @return void */ protected function _prepareLayout() @@ -63,6 +65,8 @@ protected function _prepareLayout() } /** + * Get payment info child block html. + * * @return string */ public function getPaymentInfoHtml() @@ -71,6 +75,8 @@ public function getPaymentInfoHtml() } /** + * Retrieve current order from registry. + * * @return \Magento\Sales\Model\Order|null */ public function getOrder() @@ -104,6 +110,8 @@ public function getItems() } /** + * Prepare item before output. + * * @param AbstractBlock $renderer * @return $this */ @@ -116,7 +124,7 @@ protected function _prepareItem(AbstractBlock $renderer) /** * Returns string with formatted address * - * @param Address $address + * @param \Magento\Sales\Model\Order\Address $address * @return null|string */ public function getFormattedAddress(\Magento\Sales\Model\Order\Address $address) diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php index 035dc7877897d..603aa2586b051 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php @@ -7,12 +7,15 @@ use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +/** + * Order create index page controller. + */ class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create implements HttpGetActionInterface { /** * Index page * - * @return void + * @return \Magento\Backend\Model\View\Result\Page */ public function execute() { diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php index 3295b244f323e..ceb231248ef5e 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php @@ -1,18 +1,21 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Framework\Registry; use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Service\InvoiceService; +/** + * Create new invoice action. + */ class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** @@ -37,22 +40,32 @@ class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInte */ private $invoiceService; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** * @param Action\Context $context * @param Registry $registry * @param PageFactory $resultPageFactory * @param InvoiceService $invoiceService + * @param OrderRepositoryInterface|null $orderRepository */ public function __construct( Action\Context $context, Registry $registry, PageFactory $resultPageFactory, - InvoiceService $invoiceService + InvoiceService $invoiceService, + OrderRepositoryInterface $orderRepository = null ) { + parent::__construct($context); + $this->registry = $registry; $this->resultPageFactory = $resultPageFactory; - parent::__construct($context); $this->invoiceService = $invoiceService; + $this->orderRepository = $orderRepository ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); } /** @@ -78,14 +91,11 @@ public function execute() { $orderId = $this->getRequest()->getParam('order_id'); $invoiceData = $this->getRequest()->getParam('invoice', []); - $invoiceItems = isset($invoiceData['items']) ? $invoiceData['items'] : []; + $invoiceItems = $invoiceData['items'] ?? []; try { /** @var \Magento\Sales\Model\Order $order */ - $order = $this->_objectManager->create(\Magento\Sales\Model\Order::class)->load($orderId); - if (!$order->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The order no longer exists.')); - } + $order = $this->orderRepository->get($orderId); if (!$order->canInvoice()) { throw new \Magento\Framework\Exception\LocalizedException( 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 ab74a64b6fcf3..67a0dc469163b 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -18,6 +18,8 @@ use Magento\Sales\Model\Service\InvoiceService; /** + * Save invoice controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface @@ -103,6 +105,7 @@ protected function _prepareShipment($invoice) /** * Save invoice + * * We can save only new invoice. Existing invoices are not editable * * @return \Magento\Framework\Controller\ResultInterface @@ -194,12 +197,6 @@ public function execute() } $transactionSave->save(); - if (!empty($data['do_shipment'])) { - $this->messageManager->addSuccessMessage(__('You created the invoice and shipment.')); - } else { - $this->messageManager->addSuccessMessage(__('The invoice has been created.')); - } - // send invoice/shipment emails try { if (!empty($data['send_email'])) { @@ -219,6 +216,11 @@ public function execute() $this->messageManager->addErrorMessage(__('We can\'t send the shipment right now.')); } } + if (!empty($data['do_shipment'])) { + $this->messageManager->addSuccessMessage(__('You created the invoice and shipment.')); + } else { + $this->messageManager->addSuccessMessage(__('The invoice has been created.')); + } $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); return $resultRedirect->setPath('sales/order/view', ['order_id' => $orderId]); } catch (LocalizedException $e) { diff --git a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php index 0deddd9fb6ec5..fc4e238d47c99 100644 --- a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php +++ b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php @@ -4,9 +4,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Controller\Download; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Action\Context; use Magento\Catalog\Model\Product\Type\AbstractType; @@ -14,9 +16,10 @@ /** * Class DownloadCustomOption + * * @package Magento\Sales\Controller\Download */ -class DownloadCustomOption extends \Magento\Framework\App\Action\Action +class DownloadCustomOption extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * @var ForwardFactory @@ -95,10 +98,11 @@ public function execute() /** @var $productOption \Magento\Catalog\Model\Product\Option */ $productOption = $this->_objectManager->create( \Magento\Catalog\Model\Product\Option::class - )->load($optionId); + ); + $productOption->load($optionId); } - if (!$productOption || !$productOption->getId() || $productOption->getType() != 'file') { + if ($productOption->getId() && $productOption->getType() != 'file') { return $resultForward->forward('noroute'); } @@ -118,10 +122,10 @@ public function execute() * Ends execution process * * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ protected function endExecute() { + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } } diff --git a/app/code/Magento/Sales/Controller/Guest/Form.php b/app/code/Magento/Sales/Controller/Guest/Form.php index 8b4467cb538fa..04bb66f3d5b6e 100644 --- a/app/code/Magento/Sales/Controller/Guest/Form.php +++ b/app/code/Magento/Sales/Controller/Guest/Form.php @@ -4,40 +4,72 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Controller\Guest; -class Form extends \Magento\Framework\App\Action\Action +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Helper\Guest as GuestHelper; + +/** + * Class Form + */ +class Form extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** - * @var \Magento\Framework\View\Result\PageFactory + * @var PageFactory */ protected $resultPageFactory; /** - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @var CustomerSession|null + */ + private $customerSession; + + /** + * @var GuestHelper|null + */ + private $guestHelper; + + /** + * @param Context $context + * @param PageFactory $resultPageFactory + * @param CustomerSession|null $customerSession + * @param GuestHelper|null $guestHelper */ public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + Context $context, + PageFactory $resultPageFactory, + CustomerSession $customerSession = null, + GuestHelper $guestHelper = null ) { parent::__construct($context); $this->resultPageFactory = $resultPageFactory; + $this->customerSession = $customerSession ?: ObjectManager::getInstance()->get(CustomerSession::class); + $this->guestHelper = $guestHelper ?: ObjectManager::getInstance()->get(GuestHelper::class); } /** * Order view form page * - * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\View\Result\Page + * @return Redirect|Page */ public function execute() { - if ($this->_objectManager->get(\Magento\Customer\Model\Session::class)->isLoggedIn()) { + if ($this->customerSession->isLoggedIn()) { return $this->resultRedirectFactory->create()->setPath('customer/account/'); } + $resultPage = $this->resultPageFactory->create(); $resultPage->getConfig()->getTitle()->set(__('Orders and Returns')); - $this->_objectManager->get(\Magento\Sales\Helper\Guest::class)->getBreadcrumbs($resultPage); + $this->guestHelper->getBreadcrumbs($resultPage); + return $resultPage; } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 088ad5a61f6c3..063433140566a 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -23,6 +23,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\Model\Cart\CartInterface @@ -582,6 +583,7 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) } $quote->getShippingAddress()->unsCachedItemsAll(); + $quote->getBillingAddress()->unsCachedItemsAll(); $quote->setTotalsCollectedFlag(false); $this->quoteRepository->save($quote); diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index a9c14be8675ab..48deddb2fe5ac 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -8,6 +8,7 @@ use Magento\Directory\Model\Currency; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Sales\Api\Data\OrderInterface; @@ -23,6 +24,8 @@ use Magento\Sales\Model\ResourceModel\Order\Shipment\Collection as ShipmentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection as TrackCollection; use Magento\Sales\Model\ResourceModel\Order\Status\History\Collection as HistoryCollection; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; /** * Order model @@ -286,6 +289,16 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ private $productOption; + /** + * @var OrderItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -316,6 +329,8 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param array $data * @param ResolverInterface $localeResolver * @param ProductOption|null $productOption + * @param OrderItemRepositoryInterface $itemRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -347,7 +362,9 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], ResolverInterface $localeResolver = null, - ProductOption $productOption = null + ProductOption $productOption = null, + OrderItemRepositoryInterface $itemRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -371,6 +388,10 @@ public function __construct( $this->priceCurrency = $priceCurrency; $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(ResolverInterface::class); $this->productOption = $productOption ?: ObjectManager::getInstance()->get(ProductOption::class); + $this->itemRepository = $itemRepository ?: ObjectManager::getInstance() + ->get(OrderItemRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); parent::__construct( $context, @@ -668,14 +689,14 @@ private function canCreditmemoForZeroTotalRefunded($totalRefunded) return true; } - + /** * Retrieve credit memo for zero total availability. * * @param float $totalRefunded * @return bool */ - public function canCreditmemoForZeroTotal($totalRefunded) + private function canCreditmemoForZeroTotal($totalRefunded) { $totalPaid = $this->getTotalPaid(); //check if total paid is less than grandtotal @@ -1040,10 +1061,21 @@ public function setState($state) return $this->setData(self::STATE, $state); } + /** + * Retrieve frontend label of order status + * + * @return string + */ + public function getFrontendStatusLabel() + { + return $this->getConfig()->getStatusFrontendLabel($this->getStatus()); + } + /** * Retrieve label of order status * * @return string + * @throws LocalizedException */ public function getStatusLabel() { @@ -1151,12 +1183,12 @@ public function place() * Hold order * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function hold() { if (!$this->canHold()) { - throw new \Magento\Framework\Exception\LocalizedException(__('A hold action is not available.')); + throw new LocalizedException(__('A hold action is not available.')); } $this->setHoldBeforeState($this->getState()); $this->setHoldBeforeStatus($this->getStatus()); @@ -1169,12 +1201,12 @@ public function hold() * Attempt to unhold the order * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function unhold() { if (!$this->canUnhold()) { - throw new \Magento\Framework\Exception\LocalizedException(__('You cannot remove the hold.')); + throw new LocalizedException(__('You cannot remove the hold.')); } $this->setState($this->getHoldBeforeState()) @@ -1218,7 +1250,7 @@ public function isFraudDetected() * @param string $comment * @param bool $graceful * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function registerCancellation($comment = '', $graceful = true) @@ -1257,7 +1289,7 @@ public function registerCancellation($comment = '', $graceful = true) $this->addStatusHistoryComment($comment, false); } } elseif (!$graceful) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot cancel this order.')); + throw new LocalizedException(__('We cannot cancel this order.')); } return $this; } @@ -1279,12 +1311,12 @@ public function getTrackingNumbers() * Retrieve shipping method * * @param bool $asObject return carrier code and shipping method data as object - * @return string|\Magento\Framework\DataObject + * @return string|null|\Magento\Framework\DataObject */ public function getShippingMethod($asObject = false) { $shippingMethod = parent::getShippingMethod(); - if (!$asObject) { + if (!$asObject || !$shippingMethod) { return $shippingMethod; } else { list($carrierCode, $method) = explode('_', $shippingMethod, 2); @@ -2076,9 +2108,12 @@ public function getIncrementId() public function getItems() { if ($this->getData(OrderInterface::ITEMS) == null) { + $this->searchCriteriaBuilder->addFilter(OrderItemInterface::ORDER_ID, $this->getId()); + + $searchCriteria = $this->searchCriteriaBuilder->create(); $this->setData( OrderInterface::ITEMS, - $this->getItemsCollection()->getItems() + $this->itemRepository->getList($searchCriteria)->getItems() ); } return $this->getData(OrderInterface::ITEMS); @@ -2919,7 +2954,7 @@ public function getDiscountTaxCompensationRefunded() } /** - * Return hold_before_state + * Returns hold_before_state * * @return string|null */ diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index 31cb5bb1f60ca..5d3186781e7d7 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Validator.php +++ b/app/code/Magento/Sales/Model/Order/Address/Validator.php @@ -49,8 +49,8 @@ class Validator /** * @param DirectoryHelper $directoryHelper - * @param CountryFactory $countryFactory - * @param EavConfig $eavConfig + * @param CountryFactory $countryFactory + * @param EavConfig $eavConfig */ public function __construct( DirectoryHelper $directoryHelper, @@ -61,6 +61,17 @@ public function __construct( $this->countryFactory = $countryFactory; $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(EavConfig::class); + } + + /** + * Validate address. + * + * @param \Magento\Sales\Model\Order\Address $address + * @return array + */ + public function validate(Address $address) + { + $warnings = []; if ($this->isTelephoneRequired()) { $this->required['telephone'] = 'Phone Number'; @@ -73,16 +84,7 @@ public function __construct( if ($this->isFaxRequired()) { $this->required['fax'] = 'Fax'; } - } - /** - * - * @param \Magento\Sales\Model\Order\Address $address - * @return array - */ - public function validate(Address $address) - { - $warnings = []; foreach ($this->required as $code => $label) { if (!$address->hasData($code)) { $warnings[] = sprintf('"%s" is required. Enter and try again.', $label); @@ -195,7 +197,10 @@ protected function isStateRequired($countryId) } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isTelephoneRequired() { @@ -203,7 +208,10 @@ protected function isTelephoneRequired() } /** + * Check whether company is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isCompanyRequired() { @@ -211,7 +219,10 @@ protected function isCompanyRequired() } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isFaxRequired() { diff --git a/app/code/Magento/Sales/Model/Order/AddressRepository.php b/app/code/Magento/Sales/Model/Order/AddressRepository.php index 2aed6ef16817e..af83dde99c6f2 100644 --- a/app/code/Magento/Sales/Model/Order/AddressRepository.php +++ b/app/code/Magento/Sales/Model/Order/AddressRepository.php @@ -90,7 +90,7 @@ public function get($id) * Find order addresses by criteria. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria - * @return \Magento\Sales\Api\Data\OrderAddressInterface[] + * @return \Magento\Sales\Model\ResourceModel\Order\Address\Collection */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { diff --git a/app/code/Magento/Sales/Model/Order/Config.php b/app/code/Magento/Sales/Model/Order/Config.php index e00eda647dc8d..1b31caa573f99 100644 --- a/app/code/Magento/Sales/Model/Order/Config.php +++ b/app/code/Magento/Sales/Model/Order/Config.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Model\Order; +use Magento\Framework\Exception\LocalizedException; + /** * Order configuration model * @@ -73,6 +75,8 @@ public function __construct( } /** + * Get collection. + * * @return \Magento\Sales\Model\ResourceModel\Order\Status\Collection */ protected function _getCollection() @@ -84,8 +88,10 @@ protected function _getCollection() } /** + * Get state. + * * @param string $state - * @return Status|null + * @return Status */ protected function _getState($state) { @@ -101,9 +107,9 @@ protected function _getState($state) * Retrieve default status for state * * @param string $state - * @return string + * @return string|null */ - public function getStateDefaultStatus($state) + public function getStateDefaultStatus($state): ?string { $status = false; $stateNode = $this->_getState($state); @@ -115,24 +121,48 @@ public function getStateDefaultStatus($state) } /** - * Retrieve status label + * Get status label for a specified area * - * @param string $code - * @return string + * @param string|null $code + * @param string $area + * @return string|null */ - public function getStatusLabel($code) + private function getStatusLabelForArea(?string $code, string $area): ?string { - $area = $this->state->getAreaCode(); $code = $this->maskStatusForArea($area, $code); $status = $this->orderStatusFactory->create()->load($code); - if ($area == 'adminhtml') { + if ($area === 'adminhtml') { return $status->getLabel(); } return $status->getStoreLabel(); } + /** + * Retrieve status label for detected area + * + * @param string|null $code + * @return string|null + * @throws LocalizedException + */ + public function getStatusLabel($code) + { + $area = $this->state->getAreaCode() ?: \Magento\Framework\App\Area::AREA_FRONTEND; + return $this->getStatusLabelForArea($code, $area); + } + + /** + * Retrieve status label for area + * + * @param string|null $code + * @return string|null + */ + public function getStatusFrontendLabel(?string $code): ?string + { + return $this->getStatusLabelForArea($code, \Magento\Framework\App\Area::AREA_FRONTEND); + } + /** * Mask status for order for specified area * @@ -249,8 +279,9 @@ public function getInvisibleOnFrontStatuses() } /** - * Get existing order statuses - * Visible or invisible on frontend according to passed param + * Get existing order statuses. + * + * Visible or invisible on frontend according to passed param. * * @param bool $visibility * @return array diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Creditmemo.php index cba45be6dfb35..708aee5e59261 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo.php @@ -42,11 +42,6 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt const REPORT_DATE_TYPE_REFUND_CREATED = 'refund_created'; - /** - * Allow Zero Grandtotal for Creditmemo path - */ - const XML_PATH_ALLOW_ZERO_GRANDTOTAL = 'sales/zerograndtotal_creditmemo/allow_zero_grandtotal'; - /** * Identifier for order history item * @@ -655,10 +650,10 @@ public function isValidGrandTotal() * * @return bool */ - public function isAllowZeroGrandTotal() + private function isAllowZeroGrandTotal() { $isAllowed = $this->scopeConfig->getValue( - self::XML_PATH_ALLOW_ZERO_GRANDTOTAL, + 'sales/zerograndtotal_creditmemo/allow_zero_grandtotal', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); return $isAllowed; diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php index 5c3f563a4f07e..35244b2661383 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php @@ -10,6 +10,8 @@ use Magento\Sales\Model\AbstractModel; /** + * Creditmemo item model. + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) @@ -189,6 +191,8 @@ public function register() } /** + * Calculate qty for creditmemo item. + * * @return int|float * @throws \Magento\Framework\Exception\LocalizedException */ @@ -212,6 +216,8 @@ private function processQty() } /** + * Cancel creaditmemeo item. + * * @return $this */ public function cancel() @@ -236,7 +242,7 @@ public function cancel() /** * Invoice item row total calculation * - * @return \Magento\Sales\Model\Order\Invoice\Item + * @return $this */ public function calcRowTotal() { @@ -608,7 +614,7 @@ public function getWeeeTaxRowDisposition() //@codeCoverageIgnoreStart /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -616,7 +622,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBasePrice($price) { @@ -624,7 +630,7 @@ public function setBasePrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -632,7 +638,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowTotal($amount) { @@ -640,7 +646,7 @@ public function setBaseRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -648,7 +654,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotal($amount) { @@ -656,7 +662,7 @@ public function setRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -664,7 +670,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPriceInclTax($amount) { @@ -672,7 +678,7 @@ public function setPriceInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -680,7 +686,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBasePriceInclTax($amount) { @@ -688,7 +694,7 @@ public function setBasePriceInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCost($baseCost) { @@ -696,7 +702,7 @@ public function setBaseCost($baseCost) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrice($price) { @@ -704,7 +710,7 @@ public function setPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowTotalInclTax($amount) { @@ -712,7 +718,7 @@ public function setBaseRowTotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotalInclTax($amount) { @@ -720,7 +726,7 @@ public function setRowTotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductId($id) { @@ -728,7 +734,7 @@ public function setProductId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderItemId($id) { @@ -736,7 +742,7 @@ public function setOrderItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -744,7 +750,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDescription($description) { @@ -752,7 +758,7 @@ public function setDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSku($sku) { @@ -760,7 +766,7 @@ public function setSku($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function setName($name) { @@ -768,7 +774,7 @@ public function setName($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -776,7 +782,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -784,7 +790,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxDisposition($weeeTaxDisposition) { @@ -792,7 +798,7 @@ public function setWeeeTaxDisposition($weeeTaxDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxRowDisposition($weeeTaxRowDisposition) { @@ -800,7 +806,7 @@ public function setWeeeTaxRowDisposition($weeeTaxRowDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxDisposition($baseWeeeTaxDisposition) { @@ -808,7 +814,7 @@ public function setBaseWeeeTaxDisposition($baseWeeeTaxDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxRowDisposition($baseWeeeTaxRowDisposition) { @@ -816,7 +822,7 @@ public function setBaseWeeeTaxRowDisposition($baseWeeeTaxRowDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxApplied($weeeTaxApplied) { @@ -824,7 +830,7 @@ public function setWeeeTaxApplied($weeeTaxApplied) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxAppliedAmount($amount) { @@ -832,7 +838,7 @@ public function setBaseWeeeTaxAppliedAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxAppliedRowAmnt($amnt) { @@ -840,7 +846,7 @@ public function setBaseWeeeTaxAppliedRowAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxAppliedAmount($amount) { @@ -848,7 +854,7 @@ public function setWeeeTaxAppliedAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxAppliedRowAmount($amount) { @@ -856,7 +862,7 @@ public function setWeeeTaxAppliedRowAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\CreditmemoItemExtensionInterface|null */ @@ -866,7 +872,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\CreditmemoItemExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php index d6da44c5cb5b9..3fd3eaaa11a7f 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php @@ -28,7 +28,7 @@ class ItemCreation implements CreditmemoItemCreationInterface private $extensionAttributes; /** - * {@inheritdoc} + * @inheritdoc */ public function getOrderItemId() { @@ -36,7 +36,7 @@ public function getOrderItemId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderItemId($orderItemId) { @@ -45,7 +45,7 @@ public function setOrderItemId($orderItemId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getQty() { @@ -53,7 +53,7 @@ public function getQty() } /** - * {@inheritdoc} + * @inheritdoc */ public function setQty($qty) { diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index ecd5670a319e7..3d2c13cbaaaa9 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php index 8004483583114..126fe4f93f1e0 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php @@ -57,10 +57,10 @@ class CreditmemoSender extends Sender * @param CreditmemoIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param CreditmemoResource $creditmemoResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -96,10 +96,11 @@ public function __construct( * @param Creditmemo $creditmemo * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Creditmemo $creditmemo, $forceSyncMode = false) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $creditmemo->getOrder(); @@ -146,6 +147,7 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 994fd79945cfd..ba3895cfa1524 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -57,10 +57,10 @@ class InvoiceSender extends Sender * @param InvoiceIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param InvoiceResource $invoiceResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -96,10 +96,11 @@ public function __construct( * @param Invoice $invoice * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Invoice $invoice, $forceSyncMode = false) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $invoice->getOrder(); @@ -146,6 +147,7 @@ public function send(Invoice $invoice, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php index f06da0de0fd00..bfbe1fb4fd7ff 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -55,10 +55,10 @@ class OrderSender extends Sender * @param OrderIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param OrderResource $orderResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -97,7 +97,7 @@ public function __construct( */ public function send(Order $order, $forceSyncMode = false) { - $order->setSendEmail(true); + $order->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { if ($this->checkAndSend($order)) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index 6729c746f5565..10e5e37a49394 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -57,10 +57,10 @@ class ShipmentSender extends Sender * @param ShipmentIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param ShipmentResource $shipmentResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -96,10 +96,11 @@ public function __construct( * @param Shipment $shipment * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Shipment $shipment, $forceSyncMode = false) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $shipment->getOrder(); @@ -146,6 +147,7 @@ public function send(Shipment $shipment, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index a7d749ec04c7d..ed9e38822245f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -106,7 +106,7 @@ protected function configureEmailTemplate() $this->transportBuilder->setTemplateIdentifier($this->templateContainer->getTemplateId()); $this->transportBuilder->setTemplateOptions($this->templateContainer->getTemplateOptions()); $this->transportBuilder->setTemplateVars($this->templateContainer->getTemplateVars()); - $this->transportBuilder->setFromByStore( + $this->transportBuilder->setFromByScope( $this->identityContainer->getEmailIdentity(), $this->identityContainer->getStore()->getId() ); diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index aa0687bee504f..5ae3306ddf75b 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\InvoiceCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index 97040c0a578c8..5d1d3f0d040a7 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -264,6 +264,7 @@ public function getParentTransactionId() /** * Returns transaction parent * + * @param string $txnId * @return string * @since 100.1.0 */ @@ -299,6 +300,8 @@ public function canCapture() } /** + * Check refund availability. + * * @return bool */ public function canRefund() @@ -307,6 +310,8 @@ public function canRefund() } /** + * Check partial refund availability for invoice. + * * @return bool */ public function canRefundPartialPerInvoice() @@ -315,6 +320,8 @@ public function canRefundPartialPerInvoice() } /** + * Check partial capture availability. + * * @return bool */ public function canCapturePartial() @@ -324,6 +331,7 @@ public function canCapturePartial() /** * Authorize or authorize and capture payment on gateway, if applicable + * * This method is supposed to be called only when order is placed * * @return $this @@ -538,8 +546,7 @@ public function cancelInvoice($invoice) } /** - * Create new invoice with maximum qty for invoice for each item - * register this invoice and capture + * Create new invoice with maximum qty for invoice for each item register this invoice and capture * * @return Invoice */ @@ -677,6 +684,7 @@ public function refund($creditmemo) $gateway->refund($this, $baseAmountToRefund); $creditmemo->setTransactionId($this->getLastTransId()); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { if (!$captureTxn) { throw new \Magento\Framework\Exception\LocalizedException( @@ -723,10 +731,14 @@ public function refund($creditmemo) $message = $message = $this->prependMessage($message); $message = $this->_appendTransactionToMessage($transaction, $message); $orderState = $this->getOrderStateResolver()->getStateForOrder($this->getOrder()); + $statuses = $this->getOrder()->getConfig()->getStateStatuses($orderState, false); + $status = in_array($this->getOrder()->getStatus(), $statuses, true) + ? $this->getOrder()->getStatus() + : $this->getOrder()->getConfig()->getStateDefaultStatus($orderState); $this->getOrder() ->addStatusHistoryComment( $message, - $this->getOrder()->getConfig()->getStateDefaultStatus($orderState) + $status )->setIsCustomerNotified($creditmemo->getOrder()->getCustomerNoteNotify()); $this->_eventManager->dispatch( 'sales_order_payment_refund', @@ -849,6 +861,7 @@ public function cancelCreditmemo($creditmemo) /** * Order cancellation hook for payment method instance + * * Adds void transaction if needed * * @return $this @@ -884,6 +897,8 @@ public function canReviewPayment() } /** + * Check fetch transaction info availability + * * @return bool */ public function canFetchTransactionInfo() @@ -1191,6 +1206,11 @@ public function addTransaction($type, $salesDocument = null, $failSafe = false) } /** + * Add transaction comments to order. + * + * @param Transaction|null $transaction + * @param string $message + * @return void */ public function addTransactionCommentsToOrder($transaction, $message) { @@ -1227,6 +1247,7 @@ public function importTransactionInfo(Transaction $transactionTo) /** * Totals updater utility method + * * Updates self totals by keys in data array('key' => $delta) * * @param array $data @@ -1261,6 +1282,7 @@ protected function _appendTransactionToMessage($transaction, $message) /** * Prepend a "prepared_message" that may be set to the payment instance before, to the specified message + * * Prepends value to the specified string or to the comment of specified order status history item instance * * @param string|\Magento\Sales\Model\Order\Status\History $messagePrependTo @@ -1303,6 +1325,7 @@ public function formatAmount($amount, $asFloat = false) /** * Format price with currency sign + * * @param float $amount * @return string */ @@ -1313,6 +1336,7 @@ public function formatPrice($amount) /** * Lookup an authorization transaction using parent transaction id, if set + * * @return Transaction|false */ public function getAuthorizationTransaction() @@ -1384,8 +1408,8 @@ public function resetTransactionAdditionalInfo() /** * Prepare credit memo * - * @param $amount - * @param $baseGrandTotal + * @param float $amount + * @param float $baseGrandTotal * @param false|Invoice $invoice * @return mixed */ @@ -1454,6 +1478,8 @@ protected function _getInvoiceForTransactionId($transactionId) } /** + * Get order state resolver instance. + * * @deprecated 100.2.0 * @return OrderStateResolverInterface */ @@ -1992,7 +2018,7 @@ public function getShippingRefunded() } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -2000,7 +2026,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingCaptured($baseShippingCaptured) { @@ -2008,7 +2034,7 @@ public function setBaseShippingCaptured($baseShippingCaptured) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingCaptured($shippingCaptured) { @@ -2016,7 +2042,7 @@ public function setShippingCaptured($shippingCaptured) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountRefunded($amountRefunded) { @@ -2024,7 +2050,7 @@ public function setAmountRefunded($amountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountPaid($baseAmountPaid) { @@ -2032,7 +2058,7 @@ public function setBaseAmountPaid($baseAmountPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountCanceled($amountCanceled) { @@ -2040,7 +2066,7 @@ public function setAmountCanceled($amountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountAuthorized($baseAmountAuthorized) { @@ -2048,7 +2074,7 @@ public function setBaseAmountAuthorized($baseAmountAuthorized) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountPaidOnline($baseAmountPaidOnline) { @@ -2056,7 +2082,7 @@ public function setBaseAmountPaidOnline($baseAmountPaidOnline) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountRefundedOnline($baseAmountRefundedOnline) { @@ -2064,7 +2090,7 @@ public function setBaseAmountRefundedOnline($baseAmountRefundedOnline) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -2072,7 +2098,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAmount($amount) { @@ -2080,7 +2106,7 @@ public function setShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountPaid($amountPaid) { @@ -2088,7 +2114,7 @@ public function setAmountPaid($amountPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountAuthorized($amountAuthorized) { @@ -2096,7 +2122,7 @@ public function setAmountAuthorized($amountAuthorized) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountOrdered($baseAmountOrdered) { @@ -2104,7 +2130,7 @@ public function setBaseAmountOrdered($baseAmountOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingRefunded($baseShippingRefunded) { @@ -2112,7 +2138,7 @@ public function setBaseShippingRefunded($baseShippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingRefunded($shippingRefunded) { @@ -2120,7 +2146,7 @@ public function setShippingRefunded($shippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountRefunded($baseAmountRefunded) { @@ -2128,7 +2154,7 @@ public function setBaseAmountRefunded($baseAmountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountOrdered($amountOrdered) { @@ -2136,7 +2162,7 @@ public function setAmountOrdered($amountOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountCanceled($baseAmountCanceled) { @@ -2144,7 +2170,7 @@ public function setBaseAmountCanceled($baseAmountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuotePaymentId($id) { @@ -2152,7 +2178,7 @@ public function setQuotePaymentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -2160,7 +2186,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcExpMonth($ccExpMonth) { @@ -2168,7 +2194,7 @@ public function setCcExpMonth($ccExpMonth) } /** - * {@inheritdoc} + * @inheritdoc * @deprecated 100.1.0 unused */ public function setCcSsStartYear($ccSsStartYear) @@ -2177,7 +2203,7 @@ public function setCcSsStartYear($ccSsStartYear) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckBankName($echeckBankName) { @@ -2185,7 +2211,7 @@ public function setEcheckBankName($echeckBankName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setMethod($method) { @@ -2193,7 +2219,7 @@ public function setMethod($method) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcDebugRequestBody($ccDebugRequestBody) { @@ -2201,7 +2227,7 @@ public function setCcDebugRequestBody($ccDebugRequestBody) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcSecureVerify($ccSecureVerify) { @@ -2209,7 +2235,7 @@ public function setCcSecureVerify($ccSecureVerify) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProtectionEligibility($protectionEligibility) { @@ -2217,7 +2243,7 @@ public function setProtectionEligibility($protectionEligibility) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcApproval($ccApproval) { @@ -2225,7 +2251,7 @@ public function setCcApproval($ccApproval) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcLast4($ccLast4) { @@ -2233,7 +2259,7 @@ public function setCcLast4($ccLast4) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcStatusDescription($description) { @@ -2241,7 +2267,7 @@ public function setCcStatusDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckType($echeckType) { @@ -2249,7 +2275,7 @@ public function setEcheckType($echeckType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcDebugResponseSerialized($ccDebugResponseSerialized) { @@ -2257,7 +2283,7 @@ public function setCcDebugResponseSerialized($ccDebugResponseSerialized) } /** - * {@inheritdoc} + * @inheritdoc * @deprecated 100.1.0 unused */ public function setCcSsStartMonth($ccSsStartMonth) @@ -2266,7 +2292,7 @@ public function setCcSsStartMonth($ccSsStartMonth) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckAccountType($echeckAccountType) { @@ -2274,7 +2300,7 @@ public function setEcheckAccountType($echeckAccountType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setLastTransId($id) { @@ -2282,7 +2308,7 @@ public function setLastTransId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcCidStatus($ccCidStatus) { @@ -2290,7 +2316,7 @@ public function setCcCidStatus($ccCidStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcOwner($ccOwner) { @@ -2298,7 +2324,7 @@ public function setCcOwner($ccOwner) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcType($ccType) { @@ -2306,7 +2332,7 @@ public function setCcType($ccType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPoNumber($poNumber) { @@ -2314,7 +2340,7 @@ public function setPoNumber($poNumber) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcExpYear($ccExpYear) { @@ -2322,7 +2348,7 @@ public function setCcExpYear($ccExpYear) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcStatus($ccStatus) { @@ -2330,7 +2356,7 @@ public function setCcStatus($ccStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckRoutingNumber($echeckRoutingNumber) { @@ -2338,7 +2364,7 @@ public function setEcheckRoutingNumber($echeckRoutingNumber) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAccountStatus($accountStatus) { @@ -2346,7 +2372,7 @@ public function setAccountStatus($accountStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAnetTransMethod($anetTransMethod) { @@ -2354,7 +2380,7 @@ public function setAnetTransMethod($anetTransMethod) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcDebugResponseBody($ccDebugResponseBody) { @@ -2362,7 +2388,7 @@ public function setCcDebugResponseBody($ccDebugResponseBody) } /** - * {@inheritdoc} + * @inheritdoc * @deprecated 100.1.0 unused */ public function setCcSsIssue($ccSsIssue) @@ -2371,7 +2397,7 @@ public function setCcSsIssue($ccSsIssue) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckAccountName($echeckAccountName) { @@ -2379,7 +2405,7 @@ public function setEcheckAccountName($echeckAccountName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcAvsStatus($ccAvsStatus) { @@ -2387,7 +2413,7 @@ public function setCcAvsStatus($ccAvsStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcNumberEnc($ccNumberEnc) { @@ -2395,7 +2421,7 @@ public function setCcNumberEnc($ccNumberEnc) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcTransId($id) { @@ -2403,7 +2429,7 @@ public function setCcTransId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAddressStatus($addressStatus) { @@ -2411,7 +2437,7 @@ public function setAddressStatus($addressStatus) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\OrderPaymentExtensionInterface|null */ @@ -2421,7 +2447,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\OrderPaymentExtensionInterface $extensionAttributes * @return $this @@ -2505,6 +2531,7 @@ public function getShouldCloseParentTransaction() /** * Set payment parent transaction id and current transaction id if it not set + * * @param Transaction $transaction * @return void */ @@ -2526,6 +2553,7 @@ private function setTransactionIdsForRefund(Transaction $transaction) /** * Collects order invoices totals by provided keys. + * * Returns result as {key: amount}. * * @param Order $order diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php index a8acb9aa46983..57d6c204dcafd 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php @@ -560,7 +560,7 @@ public function getOrderId() /** * Retrieve order instance * - * @return \Magento\Sales\Model\Order + * @return \Magento\Sales\Model\Order\Payment */ public function getOrder() { diff --git a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 8cdc90972bbb0..85e34f560bb7b 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -363,38 +363,6 @@ protected function _calcAddressHeight($address) return $y; } - /** - * Detect an input string is Arabic - * - * @param string $subject - * @return bool - */ - private function isArabic(string $subject): bool - { - return (preg_match('/\p{Arabic}/u', $subject) > 0); - } - - /** - * Reverse text with Arabic characters - * - * @param string $string - * @return string - */ - private function reverseArabicText($string) - { - $splitText = explode(' ', $string); - for ($i = 0; $i < count($splitText); $i++) { - if ($this->isArabic($splitText[$i])) { - for ($j = $i + 1; $j < count($splitText); $j++) { - $tmp = $this->string->strrev($splitText[$j]); - $splitText[$j] = $this->string->strrev($splitText[$i]); - $splitText[$i] = $tmp; - } - } - } - return implode(' ', $splitText); - } - /** * Insert order to pdf page * @@ -506,7 +474,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if ($value !== '') { $text = []; foreach ($this->string->split($value, 45, true, true) as $_value) { - $text[] = ($this->isArabic($_value)) ? $this->reverseArabicText($_value) : $_value; + $text[] = $_value; } foreach ($text as $part) { $page->drawText(strip_tags(ltrim($part)), 35, $this->y, 'UTF-8'); @@ -523,7 +491,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if ($value !== '') { $text = []; foreach ($this->string->split($value, 45, true, true) as $_value) { - $text[] = ($this->isArabic($_value)) ? $this->reverseArabicText($_value) : $_value; + $text[] = $_value; } foreach ($text as $part) { $page->drawText(strip_tags(ltrim($part)), 285, $this->y, 'UTF-8'); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php index 11908864236f6..48934e24a3795 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf\Items\Creditmemo; /** @@ -66,11 +68,18 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [['text' => $this->string->split($item->getName(), 35, true, true), 'feed' => 35]]; + $lines[0] = [ + [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($item->getName()), 35, true, true), + 'feed' => 35 + ] + ]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->getSku($item), 17), + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($this->getSku($item)), 17), 'feed' => 255, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 8562328025540..23c2c00daadc3 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf\Items\Invoice; /** @@ -66,11 +68,18 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [['text' => $this->string->split($item->getName(), 35, true, true), 'feed' => 35]]; + $lines[0] = [ + [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($item->getName()), 35, true, true), + 'feed' => 35 + ] + ]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->getSku($item), 17), + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($this->getSku($item)), 17), 'feed' => 290, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php index 6007e1dcf2b47..a88b508ba0789 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf\Items\Shipment; /** @@ -65,14 +67,21 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [['text' => $this->string->split($item->getName(), 60, true, true), 'feed' => 100]]; + $lines[0] = [ + [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($item->getName()), 60, true, true), + 'feed' => 100 + ] + ]; // draw QTY $lines[0][] = ['text' => $item->getQty() * 1, 'feed' => 35]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->getSku($item), 25), + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($this->getSku($item)), 25), 'feed' => 565, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index cecee4283648d..ef9c6fc628dd5 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -356,12 +356,11 @@ public function getTracksCollection() if ($this->tracksCollection === null) { $this->tracksCollection = $this->_trackCollectionFactory->create(); - if ($this->getId()) { - $this->tracksCollection->setShipmentFilter($this->getId()); + $id = $this->getId() ?: 0; + $this->tracksCollection->setShipmentFilter($id); - foreach ($this->tracksCollection as $item) { - $item->setShipment($this); - } + foreach ($this->tracksCollection as $item) { + $item->setShipment($this); } } diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index 0a393548069f5..3657f84d4445d 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php index 57566bfd789e7..38728d88ff4fa 100644 --- a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php +++ b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php @@ -9,6 +9,7 @@ use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn; +use Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer; /** * Class for changing row total in response. @@ -20,13 +21,21 @@ class ChangeOutputArray */ private $priceRenderer; + /** + * @var DefaultRenderer + */ + private $defaultRenderer; + /** * @param DefaultColumn $priceRenderer + * @param DefaultRenderer $defaultRenderer */ public function __construct( - DefaultColumn $priceRenderer + DefaultColumn $priceRenderer, + DefaultRenderer $defaultRenderer ) { $this->priceRenderer = $priceRenderer; + $this->defaultRenderer = $defaultRenderer; } /** @@ -42,6 +51,12 @@ public function execute( ): array { $result[OrderItemInterface::ROW_TOTAL] = $this->priceRenderer->getTotalAmount($dataObject); $result[OrderItemInterface::BASE_ROW_TOTAL] = $this->priceRenderer->getBaseTotalAmount($dataObject); + $result[OrderItemInterface::ROW_TOTAL_INCL_TAX] = $this->defaultRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL_INCL_TAX] = $dataObject->getBaseRowTotal() + + $dataObject->getBaseTaxAmount() + + $dataObject->getBaseDiscountTaxCompensationAmount() + + $dataObject->getBaseWeeeTaxAppliedAmount() + - $dataObject->getBaseDiscountAmount(); return $result; } diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index a98d6193402a9..79548cb190754 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -6,7 +6,9 @@ namespace Magento\Sales\Model; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Api\Data\OrderExtensionFactory; @@ -16,8 +18,10 @@ use Magento\Sales\Api\Data\ShippingAssignmentInterface; use Magento\Sales\Model\Order\ShippingAssignmentBuilder; use Magento\Sales\Model\ResourceModel\Metadata; -use Magento\Framework\App\ObjectManager; use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; +use Magento\Framework\Serialize\Serializer\Json as JsonSerializer; /** * Repository class @@ -61,6 +65,21 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface */ private $orderTaxManagement; + /** + * @var PaymentAdditionalInfoFactory + */ + private $paymentAdditionalInfoFactory; + + /** + * @var JsonSerializer + */ + private $serializer; + + /** + * @var JoinProcessorInterface + */ + private $extensionAttributesJoinProcessor; + /** * Constructor * @@ -69,13 +88,19 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface * @param CollectionProcessorInterface|null $collectionProcessor * @param \Magento\Sales\Api\Data\OrderExtensionFactory|null $orderExtensionFactory * @param OrderTaxManagementInterface|null $orderTaxManagement + * @param PaymentAdditionalInfoInterfaceFactory|null $paymentAdditionalInfoFactory + * @param JsonSerializer|null $serializer + * @param JoinProcessorInterface $extensionAttributesJoinProcessor */ public function __construct( Metadata $metadata, SearchResultFactory $searchResultFactory, CollectionProcessorInterface $collectionProcessor = null, \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory = null, - OrderTaxManagementInterface $orderTaxManagement = null + OrderTaxManagementInterface $orderTaxManagement = null, + PaymentAdditionalInfoInterfaceFactory $paymentAdditionalInfoFactory = null, + JsonSerializer $serializer = null, + JoinProcessorInterface $extensionAttributesJoinProcessor = null ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; @@ -85,6 +110,12 @@ public function __construct( ->get(\Magento\Sales\Api\Data\OrderExtensionFactory::class); $this->orderTaxManagement = $orderTaxManagement ?: ObjectManager::getInstance() ->get(OrderTaxManagementInterface::class); + $this->paymentAdditionalInfoFactory = $paymentAdditionalInfoFactory ?: ObjectManager::getInstance() + ->get(PaymentAdditionalInfoInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(JsonSerializer::class); + $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor + ?: ObjectManager::getInstance()->get(JoinProcessorInterface::class); } /** @@ -110,6 +141,7 @@ public function get($id) } $this->setOrderTaxDetails($entity); $this->setShippingAssignments($entity); + $this->setPaymentAdditionalInfo($entity); $this->registry[$id] = $entity; } return $this->registry[$id]; @@ -138,6 +170,34 @@ private function setOrderTaxDetails(OrderInterface $order) $order->setExtensionAttributes($extensionAttributes); } + /** + * Set additional info to the order. + * + * @param OrderInterface $order + * @return void + */ + private function setPaymentAdditionalInfo(OrderInterface $order): void + { + $extensionAttributes = $order->getExtensionAttributes(); + $paymentAdditionalInformation = $order->getPayment()->getAdditionalInformation(); + + $objects = []; + foreach ($paymentAdditionalInformation as $key => $value) { + /** @var PaymentAdditionalInfoInterface $additionalInformationObject */ + $additionalInformationObject = $this->paymentAdditionalInfoFactory->create(); + $additionalInformationObject->setKey($key); + + if (!is_string($value)) { + $value = $this->serializer->serialize($value); + } + $additionalInformationObject->setValue($value); + + $objects[] = $additionalInformationObject; + } + $extensionAttributes->setPaymentAdditionalInfo($objects); + $order->setExtensionAttributes($extensionAttributes); + } + /** * Find entities by criteria * @@ -148,11 +208,13 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr { /** @var \Magento\Sales\Api\Data\OrderSearchResultInterface $searchResult */ $searchResult = $this->searchResultFactory->create(); + $this->extensionAttributesJoinProcessor->process($searchResult); $this->collectionProcessor->process($searchCriteria, $searchResult); $searchResult->setSearchCriteria($searchCriteria); foreach ($searchResult->getItems() as $order) { $this->setShippingAssignments($order); $this->setOrderTaxDetails($order); + $this->setPaymentAdditionalInfo($order); } return $searchResult; } diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php index d79f5ecf857cb..07555cba1b7f7 100644 --- a/app/code/Magento/Sales/Model/RefundOrder.php +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Model; use Magento\Framework\App\ResourceConnection; @@ -151,10 +152,13 @@ public function execute( $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); $order->setCustomerNoteNotify($notify); $order = $this->refundAdapter->refund($creditmemo, $order); - $order->setState( - $this->orderStateResolver->getStateForOrder($order, []) - ); - $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + $orderState = $this->orderStateResolver->getStateForOrder($order, []); + $order->setState($orderState); + $statuses = $this->config->getStateStatuses($orderState, false); + $status = in_array($order->getStatus(), $statuses, true) + ? $order->getStatus() + : $this->config->getStateDefaultStatus($orderState); + $order->setStatus($status); $order = $this->orderRepository->save($order); $creditmemo = $this->creditmemoRepository->save($creditmemo); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php index 5dca23836427a..6ad8ebc3bb89d 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php @@ -48,7 +48,7 @@ class Collection extends AbstractCollection implements OrderSearchResultInterfac * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot * @param \Magento\Framework\DB\Helper $coreResourceHelper - * @param string|null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource */ public function __construct( @@ -138,6 +138,7 @@ protected function _getAllIdsSelect($limit = null, $offset = null) /** * Join table sales_order_address to select for billing and shipping order addresses. + * * Create correlation map * * @return $this diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index 2f5a176b9617a..de15a627583ff 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -31,7 +31,10 @@ public function check(Order $order) } if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice()) { - if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) && !$order->canCreditmemo()) { + if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) + && !$order->canCreditmemo() + && !$order->canShip() + ) { $order->setState(Order::STATE_CLOSED) ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); } elseif ($currentState === Order::STATE_PROCESSING && !$order->canShip()) { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php index 521db7f1f3a45..fead4f39f4c2f 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php @@ -33,7 +33,7 @@ class Collection extends AbstractCollection implements OrderPaymentSearchResultI * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource */ public function __construct( diff --git a/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php b/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php index 86d86255a0c29..9c4c87c2e2e25 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php @@ -25,7 +25,7 @@ class AbstractCollection extends \Magento\Reports\Model\ResourceModel\Report\Col * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Sales\Model\ResourceModel\Report $resource - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php index 76db717161317..e4435d3481a3c 100644 --- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php +++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php @@ -98,7 +98,7 @@ public function __construct( * Cancel an existing creditmemo * * @param int $id Credit Memo Id - * @return bool + * @return void * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/code/Magento/Sales/Model/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index 2806f76b1389b..18efeba726c1b 100644 --- a/app/code/Magento/Sales/Model/Service/InvoiceService.php +++ b/app/code/Magento/Sales/Model/Service/InvoiceService.php @@ -7,9 +7,14 @@ use Magento\Sales\Api\InvoiceManagementInterface; use Magento\Sales\Model\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Model\Product\Type; /** * Class InvoiceService + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class InvoiceService implements InvoiceManagementInterface { @@ -58,6 +63,13 @@ class InvoiceService implements InvoiceManagementInterface */ protected $orderConverter; + /** + * Serializer interface instance. + * + * @var Json + */ + private $serializer; + /** * Constructor * @@ -68,6 +80,7 @@ class InvoiceService implements InvoiceManagementInterface * @param \Magento\Sales\Model\Order\InvoiceNotifier $notifier * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository * @param \Magento\Sales\Model\Convert\Order $orderConverter + * @param Json|null $serializer */ public function __construct( \Magento\Sales\Api\InvoiceRepositoryInterface $repository, @@ -76,7 +89,8 @@ public function __construct( \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Sales\Model\Order\InvoiceNotifier $notifier, \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, - \Magento\Sales\Model\Convert\Order $orderConverter + \Magento\Sales\Model\Convert\Order $orderConverter, + Json $serializer = null ) { $this->repository = $repository; $this->commentRepository = $commentRepository; @@ -85,6 +99,7 @@ public function __construct( $this->invoiceNotifier = $notifier; $this->orderRepository = $orderRepository; $this->orderConverter = $orderConverter; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } /** @@ -134,6 +149,7 @@ public function setVoid($id) */ public function prepareInvoice(Order $order, array $qtys = []) { + $isQtysEmpty = empty($qtys); $invoice = $this->orderConverter->toInvoice($order); $totalQty = 0; $qtys = $this->prepareItemsQty($order, $qtys); @@ -142,11 +158,11 @@ public function prepareInvoice(Order $order, array $qtys = []) continue; } $item = $this->orderConverter->itemToInvoiceItem($orderItem); - if ($orderItem->isDummy()) { - $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; - } elseif (isset($qtys[$orderItem->getId()])) { + if (isset($qtys[$orderItem->getId()])) { $qty = (double) $qtys[$orderItem->getId()]; - } elseif (empty($qtys)) { + } elseif ($orderItem->isDummy()) { + $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; + } elseif ($isQtysEmpty) { $qty = $orderItem->getQtyToInvoice(); } else { $qty = 0; @@ -172,25 +188,74 @@ private function prepareItemsQty(Order $order, array $qtys = []) { foreach ($order->getAllItems() as $orderItem) { if (empty($qtys[$orderItem->getId()])) { - continue; + if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) { + $qtys[$orderItem->getId()] = $orderItem->getQtyOrdered() - $orderItem->getQtyInvoiced(); + } else { + $parentItem = $orderItem->getParentItem(); + $parentItemId = $parentItem ? $parentItem->getId() : null; + if ($parentItemId && isset($qtys[$parentItemId])) { + $qtys[$orderItem->getId()] = $qtys[$parentItemId]; + } + continue; + } } - if ($orderItem->isDummy()) { - if ($orderItem->getHasChildren()) { - foreach ($orderItem->getChildrenItems() as $child) { - if (!isset($qtys[$child->getId()])) { - $qtys[$child->getId()] = $child->getQtyToInvoice(); - } + + $this->prepareItemQty($orderItem, $qtys); + } + + return $qtys; + } + + /** + * Prepare qty_invoiced for order item + * + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem + * @param array $qtys + */ + private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys) + { + $this->prepareBundleQty($orderItem, $qtys); + + if ($orderItem->isDummy()) { + if ($orderItem->getHasChildren()) { + foreach ($orderItem->getChildrenItems() as $child) { + if (!isset($qtys[$child->getId()])) { + $qtys[$child->getId()] = $child->getQtyToInvoice(); } - } elseif ($orderItem->getParentItem()) { - $parent = $orderItem->getParentItem(); - if (!isset($qtys[$parent->getId()])) { - $qtys[$parent->getId()] = $parent->getQtyToInvoice(); + $parentId = $orderItem->getParentItemId(); + if ($parentId && array_key_exists($parentId, $qtys)) { + $qtys[$orderItem->getId()] = $qtys[$parentId]; + } else { + continue; } } + } elseif ($orderItem->getParentItem()) { + $parent = $orderItem->getParentItem(); + if (!isset($qtys[$parent->getId()])) { + $qtys[$parent->getId()] = $parent->getQtyToInvoice(); + } } } + } - return $qtys; + /** + * Prepare qty to invoice for bundle products + * + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem + * @param array $qtys + */ + private function prepareBundleQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys) + { + if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) { + foreach ($orderItem->getChildrenItems() as $childItem) { + $bundleSelectionAttributes = $childItem->getProductOptionByCode('bundle_selection_attributes'); + if (is_string($bundleSelectionAttributes)) { + $bundleSelectionAttributes = $this->serializer->unserialize($bundleSelectionAttributes); + } + + $qtys[$childItem->getId()] = $qtys[$orderItem->getId()] * $bundleSelectionAttributes['qty']; + } + } } /** diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php index cade86d18e935..5883bde175101 100644 --- a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -31,7 +31,7 @@ public function __construct(OrderRepositoryInterface $orderRepository) } /** - * {@inheritdoc} + * @inheritdoc */ public function execute(Observer $observer) { @@ -44,9 +44,16 @@ public function execute(Observer $observer) $orderId = $delegateData['__sales_assign_order_id']; $order = $this->orderRepository->get($orderId); if (!$order->getCustomerId()) { - //if customer ID wasn't already assigned then assigning. - $order->setCustomerId($customer->getId()); - $order->setCustomerIsGuest(0); + //assign customer info to order after customer creation. + $order->setCustomerId($customer->getId()) + ->setCustomerIsGuest(0) + ->setCustomerEmail($customer->getEmail()) + ->setCustomerFirstname($customer->getFirstname()) + ->setCustomerLastname($customer->getLastname()) + ->setCustomerMiddlename($customer->getMiddlename()) + ->setCustomerPrefix($customer->getPrefix()) + ->setCustomerSuffix($customer->getSuffix()) + ->setCustomerGroupId($customer->getGroupId()); $this->orderRepository->save($order); } } diff --git a/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php b/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php index 2716e860243bf..a75690536e760 100644 --- a/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php +++ b/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php @@ -8,15 +8,15 @@ use Magento\Eav\Model\Config; use Magento\Framework\App\State; -use Magento\Quote\Model\QuoteFactory; -use Magento\Sales\Model\OrderFactory; -use Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory as AddressCollectionFactory; -use Magento\Framework\App\ResourceConnection; -use Magento\Sales\Setup\SalesSetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Setup\SalesSetupFactory; +/** + * Fills quote_address_id in table sales_order_address if it is empty. + */ class FillQuoteAddressIdInSalesOrderAddress implements DataPatchInterface, PatchVersionInterface { /** @@ -24,11 +24,6 @@ class FillQuoteAddressIdInSalesOrderAddress implements DataPatchInterface, Patch */ private $moduleDataSetup; - /** - * @var SalesSetupFactory - */ - private $salesSetupFactory; - /** * @var State */ @@ -40,44 +35,22 @@ class FillQuoteAddressIdInSalesOrderAddress implements DataPatchInterface, Patch private $eavConfig; /** - * @var AddressCollectionFactory - */ - private $addressCollectionFactory; - - /** - * @var OrderFactory - */ - private $orderFactory; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * PatchInitial constructor. * @param ModuleDataSetupInterface $moduleDataSetup + * @param State $state + * @param Config $eavConfig */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - SalesSetupFactory $salesSetupFactory, State $state, - Config $eavConfig, - AddressCollectionFactory $addressCollectionFactory, - OrderFactory $orderFactory, - QuoteFactory $quoteFactory + Config $eavConfig ) { $this->moduleDataSetup = $moduleDataSetup; - $this->salesSetupFactory = $salesSetupFactory; $this->state = $state; $this->eavConfig = $eavConfig; - $this->addressCollectionFactory = $addressCollectionFactory; - $this->orderFactory = $orderFactory; - $this->quoteFactory = $quoteFactory; } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -96,32 +69,12 @@ public function apply() */ public function fillQuoteAddressIdInSalesOrderAddress(ModuleDataSetupInterface $setup) { - $addressTable = $setup->getTable('sales_order_address'); - $updateOrderAddress = $setup->getConnection() - ->select() - ->joinInner( - ['sales_order' => $setup->getTable('sales_order')], - $addressTable . '.parent_id = sales_order.entity_id', - ['quote_address_id' => 'quote_address.address_id'] - ) - ->joinInner( - ['quote_address' => $setup->getTable('quote_address')], - 'sales_order.quote_id = quote_address.quote_id - AND ' . $addressTable . '.address_type = quote_address.address_type', - [] - ) - ->where( - $addressTable . '.quote_address_id IS NULL' - ); - $updateOrderAddress = $setup->getConnection()->updateFromSelect( - $updateOrderAddress, - $addressTable - ); - $setup->getConnection()->query($updateOrderAddress); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_SHIPPING); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_BILLING); } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -131,7 +84,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -139,10 +92,99 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { return []; } + + /** + * Fill quote_address_id in sales_order_address by type. + * + * @param ModuleDataSetupInterface $setup + * @param string $addressType + * @throws \Zend_Db_Statement_Exception + */ + private function fillQuoteAddressIdInSalesOrderAddressByType(ModuleDataSetupInterface $setup, $addressType) + { + $salesConnection = $setup->getConnection('sales'); + + $orderTable = $setup->getTable('sales_order', 'sales'); + $orderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $salesConnection + ->select() + ->from( + ['sales_order_address' => $orderAddressTable], + ['entity_id', 'address_type'] + ) + ->joinInner( + ['sales_order' => $orderTable], + 'sales_order_address.parent_id = sales_order.entity_id', + ['quote_id' => 'sales_order.quote_id'] + ) + ->where('sales_order_address.quote_address_id IS NULL') + ->where('sales_order_address.address_type = ?', $addressType) + ->order('sales_order_address.entity_id'); + + $batchSize = 5000; + $result = $salesConnection->query($query); + $count = $result->rowCount(); + $batches = ceil($count / $batchSize); + + for ($batch = $batches; $batch > 0; $batch--) { + $query->limitPage($batch, $batchSize); + $result = $salesConnection->fetchAssoc($query); + + $this->fillQuoteAddressIdInSalesOrderAddressProcessBatch($setup, $result, $addressType); + } + } + + /** + * Process filling quote_address_id in sales_order_address in batch. + * + * @param ModuleDataSetupInterface $setup + * @param array $orderAddresses + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressProcessBatch( + ModuleDataSetupInterface $setup, + array $orderAddresses, + $addressType + ) { + $salesConnection = $setup->getConnection('sales'); + $quoteConnection = $setup->getConnection('checkout'); + + $quoteAddressTable = $setup->getTable('quote_address', 'checkout'); + $quoteTable = $setup->getTable('quote', 'checkout'); + $salesOrderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $quoteConnection + ->select() + ->from( + ['quote_address' => $quoteAddressTable], + ['quote_id', 'address_id'] + ) + ->joinInner( + ['quote' => $quoteTable], + 'quote_address.quote_id = quote.entity_id', + [] + ) + ->where('quote.entity_id in (?)', array_column($orderAddresses, 'quote_id')) + ->where('address_type = ?', $addressType); + + $quoteAddresses = $quoteConnection->fetchAssoc($query); + + foreach ($orderAddresses as $orderAddress) { + $bind = [ + 'quote_address_id' => $quoteAddresses[$orderAddress['quote_id']]['address_id'] ?? null, + ]; + $where = [ + 'entity_id = ?' => $orderAddress['entity_id'] + ]; + + $salesConnection->update($salesOrderAddressTable, $bind, $where); + } + } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml index c814a886a2b33..b90bac7e0881b 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml @@ -19,18 +19,15 @@ <see selector="{{AdminInvoiceOrderInformationSection.customerName}}" userInput="{{customer.firstname}}" stepKey="seeCustomerName"/> <see selector="{{AdminInvoiceOrderInformationSection.customerEmail}}" userInput="{{customer.email}}" stepKey="seeCustomerEmail"/> <see selector="{{AdminInvoiceOrderInformationSection.customerGroup}}" userInput="{{customerGroup.code}}" stepKey="seeCustomerGroup"/> - <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.street[0]}}" stepKey="seeBillingAddressStreet"/> <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.city}}" stepKey="seeBillingAddressCity"/> <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.country_id}}" stepKey="seeBillingAddressCountry"/> <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.postcode}}" stepKey="seeBillingAddressPostcode"/> - <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.street[0]}}" stepKey="seeShippingAddressStreet"/> <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.city}}" stepKey="seeShippingAddressCity"/> <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.country_id}}" stepKey="seeShippingAddressCountry"/> <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.postcode}}" stepKey="seeShippingAddressPostcode"/> </actionGroup> - <!--Check that product is in invoice items--> <actionGroup name="seeProductInInvoiceItems"> <arguments> @@ -38,29 +35,49 @@ </arguments> <see selector="{{AdminInvoiceItemsSection.skuColumn}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> </actionGroup> - <!--Admin Fast Create Invoice--> <actionGroup name="adminFastCreateInvoice"> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> <waitForPageLoad stepKey="waitForNewInvoicePageLoad"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForSuccessMessageLoad"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoices"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5" /> <click selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="openInvoicePage"/> <waitForPageLoad stepKey="waitForInvoicePageLoad"/> </actionGroup> - + <actionGroup name="clearInvoicesGridFilters"> + <amOnPage url="{{AdminInvoicesPage.url}}" stepKey="goToInvoices"/> + <waitForPageLoad stepKey="waitInvoicesGridToLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearFilters" /> + <waitForPageLoad stepKey="waitInvoicesGrid"/> + </actionGroup> <actionGroup name="goToInvoiceIntoOrder"> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> <seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" stepKey="seeOrderInvoiceUrl"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> </actionGroup> - - <actionGroup name="submitInvoiceIntoOrder"> + <actionGroup name="StartCreateInvoiceFromOrderPage"> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" stepKey="seeNewInvoiceUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoicePageTitle"/> + </actionGroup> + <actionGroup name="SubmitInvoice"> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPageInvoice"/> - <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageInvoice"/> + </actionGroup> + <!--Filter invoices by order id --> + <actionGroup name="filterInvoiceGridByOrderId"> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + <amOnPage url="{{AdminInvoicesPage.url}}" stepKey="goToInvoices"/> + <click selector="{{AdminInvoicesGridSection.filter}}" stepKey="clickFilter"/> + <fillField selector="{{AdminInvoicesFiltersSection.orderNum}}" userInput="{{orderId}}" stepKey="fillOrderIdForFilter"/> + <click selector="{{AdminInvoicesFiltersSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForFiltersApply"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 2bb8b34aa01c8..0e09f3933c1aa 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -18,7 +18,8 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> - <click selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="selectDefaultStoreView"/> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect" /> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> @@ -59,6 +60,15 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> + <!--Navigate to New Order Page for existing Customer And Store--> + <actionGroup name="NavigateToNewOrderPageExistingCustomerAndStoreActionGroup" extends="navigateToNewOrderPageExistingCustomer" > + <arguments> + <argument name="storeView" defaultValue="_defaultStore"/> + </arguments> + <click selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="selectStoreView" after="waitForCreateOrderPageLoad"/> + <waitForPageLoad stepKey="waitForLoad" after="selectStoreView"/> + </actionGroup> + <!--Check the required fields are actually required--> <actionGroup name="checkRequiredFieldsNewOrderForm"> <seeElement selector="{{AdminOrderFormAccountSection.requiredGroup}}" stepKey="seeCustomerGroupRequired"/> @@ -341,12 +351,15 @@ <!--Cancel order that is in pending status--> <actionGroup name="cancelPendingOrder"> + <arguments> + <argument name="orderStatus" type="string" defaultValue="Canceled"/> + </arguments> <click selector="{{AdminOrderDetailsMainActionsSection.cancel}}" stepKey="clickCancelOrder"/> <waitForElement selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForCancelConfirmation"/> <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to cancel this order?" stepKey="seeConfirmationMessage"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmOrderCancel"/> <see selector="{{AdminMessagesSection.success}}" userInput="You canceled the order." stepKey="seeCancelSuccessMessage"/> - <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Canceled" stepKey="seeOrderStatusCanceled"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{orderStatus}}" stepKey="seeOrderStatusCanceled"/> </actionGroup> <!--Select Check Money payment method--> @@ -354,4 +367,26 @@ <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> <conditionalClick selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" dependentSelector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" visible="true" stepKey="checkCheckMoneyOption"/> </actionGroup> + + <!-- Create Order --> + <actionGroup name="CreateOrderActionGroup"> + <arguments> + <argument name="product"/> + <argument name="customer"/> + </arguments> + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> + <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> + <waitForPageLoad stepKey="waitForStoresPageOpened"/> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> + <waitForPageLoad stepKey="waitForProductsListForOrder"/> + <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addSelectedProductToOrder"/> + <waitForPageLoad stepKey="waitForProductAddedInOrder"/> + <click selector="{{AdminInvoicePaymentShippingSection.getShippingMethodAndRates}}" stepKey="openShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{AdminInvoicePaymentShippingSection.shippingMethod}}" stepKey="chooseShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethodsThickened"/> + <click selector="{{OrdersGridSection.submitOrder}}" stepKey="submitOrder"/> + <see stepKey="seeSuccessMessageForOrder" userInput="You created the order."/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml index eed9f80c251c8..a116a23dc02cd 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml @@ -14,7 +14,6 @@ <argument name="orderId" type="string"/> </arguments> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> - <waitForPageLoad stepKey="waitForOrderGridLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> @@ -74,4 +73,9 @@ <waitForPageLoad stepKey="waitForPageToLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.enabledFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> </actionGroup> + + <actionGroup name="OpenOrderById" extends="filterOrderGridById"> + <click selector="{{AdminDataGridTableSection.firstRow}}" after="clickOrderApplyFilters" stepKey="openOrderViewPage"/> + <waitForPageLoad after="openOrderViewPage" stepKey="waitForOrderViewPageOpened"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml new file mode 100644 index 0000000000000..8108577145421 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.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"> + <!-- Fill Order status form and click save --> + <actionGroup name="AdminOrderStatusFormFillAndSave"> + <arguments> + <argument name="status" type="string" /> + <argument name="label" type="string" /> + </arguments> + + <fillField stepKey="fillStatusCode" selector="{{AdminOrderStatusFormSection.statusCodeField}}" userInput="{{status}}"/> + <fillField stepKey="fillStatusLabel" selector="{{AdminOrderStatusFormSection.statusLabelField}}" userInput="{{label}}"/> + <click stepKey="clickSaveStatus" selector="{{AdminMainActionsSection.save}}"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml new file mode 100644 index 0000000000000..5f69f52987688 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml @@ -0,0 +1,24 @@ +<?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"> + <!-- Search order status grid for item with a specific code and validate data --> + <actionGroup name="AssertOrderStatusExistsInGrid"> + <arguments> + <argument name="status" type="string" /> + <argument name="label" type="string" /> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters"/> + <fillField selector="{{AdminOrderStatusGridSection.statusCodeFilterField}}" userInput="{{status}}" stepKey="fillStatusFilter"/> + <click selector="{{AdminSecondaryGridSection.searchButton}}" stepKey="clickSearch"/> + <see selector="{{AdminOrderStatusGridSection.statusCodeDataColumn}}" userInput="{{status}}" stepKey="seeStatusCodeInGrid"/> + <see selector="{{AdminOrderStatusGridSection.statusLabelDataColumn}}" userInput="{{label}}" stepKey="seeStatusLabelInGrid"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml new file mode 100644 index 0000000000000..5b4c3115744c9 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Assert that order status is not saved with duplication error message --> + <actionGroup name="AssertOrderStatusFormSaveDuplicateError"> + <see selector="{{AdminMessagesSection.error}}" userInput="We found another order status with the same order status code." stepKey="seeError"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml new file mode 100644 index 0000000000000..d82f4b9dd25e8 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml @@ -0,0 +1,15 @@ +<?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"> + <!-- Assert that order status saved with success message --> + <actionGroup name="AssertOrderStatusFormSaveSuccess"> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the order status." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml new file mode 100644 index 0000000000000..abc5698cc71e6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml @@ -0,0 +1,39 @@ +<?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="useBraintreeForMasterCard"> + <click stepKey="chooseBraintree" selector="{{NewOrderSection.creditCardBraintree}}"/> + <waitForPageLoad stepKey="waitForBraintreeConfigs"/> + <click stepKey="openCardTypes" selector="{{NewOrderSection.openCardTypes}}"/> + <waitForPageLoad stepKey="waitForCardTypes"/> + <click stepKey="chooseCardType" selector="{{NewOrderSection.masterCard}}"/> + <waitForPageLoad stepKey="waitForCardSelected"/> + + <switchToIFrame stepKey="switchToCardNumber" selector="{{NewOrderSection.cardFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.creditCardNumber}}" stepKey="waitForFillCardNumber"/> + <fillField stepKey="fillCardNumber" selector="{{NewOrderSection.creditCardNumber}}" userInput="{{PaymentAndShippingInfo.cardNumber}}"/> + <switchToIFrame stepKey="switchBackFromCard"/> + + <switchToIFrame stepKey="switchToExpirationMonth" selector="{{NewOrderSection.monthFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.expirationMonth}}" stepKey="waitForFillMonth"/> + <fillField stepKey="fillMonth" selector="{{NewOrderSection.expirationMonth}}" userInput="{{PaymentAndShippingInfo.month}}"/> + <switchToIFrame stepKey="switchBackFromMonth"/> + + <switchToIFrame stepKey="switchToExpirationYear" selector="{{NewOrderSection.yearFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.expirationYear}}" stepKey="waitForFillYear"/> + <fillField stepKey="fillYear" selector="{{NewOrderSection.expirationYear}}" userInput="{{PaymentAndShippingInfo.year}}"/> + <switchToIFrame stepKey="switchBackFromYear"/> + + <switchToIFrame stepKey="switchToCVV" selector="{{NewOrderSection.cvvFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.cvv}}" stepKey="waitForFillCVV"/> + <fillField stepKey="fillCVV" selector="{{NewOrderSection.cvv}}" userInput="{{PaymentAndShippingInfo.cvv}}"/> + <switchToIFrame stepKey="switchBackFromCVV"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml deleted file mode 100644 index c46dd612022fd..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?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"> - <!--Fill order information fields and click continue--> - <actionGroup name="StorefrontFillOrderInformationActionGroup"> - <arguments> - <argument name="orderId" type="string"/> - <argument name="orderLastName"/> - <argument name="orderEmail"/> - </arguments> - <amOnPage url="{{StorefrontOrdersAndReturnsPage.url}}" stepKey="navigateToOrderAndReturnPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <fillField selector="{{StorefrontOrderAndReturnInformationSection.orderId}}" userInput="{{orderId}}" stepKey="fillOrderId"/> - <fillField selector="{{StorefrontOrderAndReturnInformationSection.bilingLastName}}" userInput="{{orderLastName}}" stepKey="fillBillingLastName"/> - <fillField selector="{{StorefrontOrderAndReturnInformationSection.email}}" userInput="{{orderEmail}}" stepKey="fillEmail"/> - <click selector="{{StorefrontOrderAndReturnInformationSection.continueButton}}" stepKey="clickContinue"/> - <waitForPageLoad stepKey="waitForOrderInformationPageLoad"/> - <seeInCurrentUrl url="{{StorefrontOrderInformationPage.url}}" stepKey="seeOrderInformationUrl"/> - </actionGroup> - - <!--Enter quantity to return and submit--> - <actionGroup name="StorefrontFillQuantityToReturnActionGroup"> - <click selector="{{StorefrontOrderInformationMainSection.return}}" stepKey="gotToCreateNewReturnPage"/> - <waitForPageLoad stepKey="waitForReturnPageLoad"/> - <fillField selector="{{StorefrontCreateNewReturnMainSection.quantityToReturn}}" userInput="1" stepKey="fillQuantityToReturn"/> - <click selector="{{StorefrontCreateNewReturnMainSection.submit}}" stepKey="clickSubmit"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml new file mode 100644 index 0000000000000..fcea25f997591 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.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"> + <!--Fill order information fields and click continue--> + <actionGroup name="StorefrontSearchGuestOrderActionGroup"> + <arguments> + <argument name="orderId" type="string"/> + <argument name="orderLastName" type="string"/> + <argument name="orderEmail" type="string"/> + </arguments> + <amOnPage url="{{StorefrontOrdersAndReturnsPage.url}}" stepKey="navigateToOrderAndReturnPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{StorefrontOrderAndReturnInformationSection.orderId}}" userInput="{{orderId}}" stepKey="fillOrderId"/> + <fillField selector="{{StorefrontOrderAndReturnInformationSection.bilingLastName}}" userInput="{{orderLastName}}" stepKey="fillBillingLastName"/> + <fillField selector="{{StorefrontOrderAndReturnInformationSection.email}}" userInput="{{orderEmail}}" stepKey="fillEmail"/> + <click selector="{{StorefrontOrderAndReturnInformationSection.continueButton}}" stepKey="clickContinue"/> + <waitForPageLoad stepKey="waitForOrderInformationPageLoad"/> + <seeInCurrentUrl url="{{StorefrontOrderInformationPage.url}}" stepKey="seeOrderInformationUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Sales/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..4f6faccbb26d4 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,41 @@ +<?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="AdminMenuSalesCreditMemos"> + <data key="pageTitle">Credit Memos</data> + <data key="title">Credit Memos</data> + <data key="dataUiId">magento-sales-sales-creditmemo</data> + </entity> + <entity name="AdminMenuSalesInvoices"> + <data key="pageTitle">Invoices</data> + <data key="title">Invoices</data> + <data key="dataUiId">magento-sales-sales-invoice</data> + </entity> + <entity name="AdminMenuSalesOrders"> + <data key="pageTitle">Orders</data> + <data key="title">Orders</data> + <data key="dataUiId">magento-sales-sales-order</data> + </entity> + <entity name="AdminMenuSalesShipments"> + <data key="pageTitle">Shipments</data> + <data key="title">Shipments</data> + <data key="dataUiId">magento-sales-sales-shipment</data> + </entity> + <entity name="AdminMenuSalesTransactions"> + <data key="pageTitle">Transactions</data> + <data key="title">Transactions</data> + <data key="dataUiId">magento-sales-sales-transactions</data> + </entity> + <entity name="AdminMenuStoresSettingsOrderStatus"> + <data key="pageTitle">Order Status</data> + <data key="title">Order Status</data> + <data key="dataUiId">magento-sales-system-order-statuses</data> + </entity> +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml new file mode 100644 index 0000000000000..aecd7fcf1b703 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml @@ -0,0 +1,23 @@ +<?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="defaultOrderStatus"> + <data key="status" unique="suffix">order_status</data> + <data key="label" unique="suffix">orderLabel</data> + </entity> + <entity name="duplicatingCodeOrderStatus"> + <data key="status">pending</data> + <data key="label" unique="suffix">orderLabel</data> + </entity> + <entity name="duplicatingLabelOrderStatus"> + <data key="status" unique="suffix">order_status</data> + <data key="label">Suspected Fraud</data> + </entity> +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/SalesEnableRMAStorefrontConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/SalesEnableRMAStorefrontConfigData.xml deleted file mode 100644 index 76ff20813483e..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Data/SalesEnableRMAStorefrontConfigData.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * 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="EnableRMA" type="sales_rma_config"> - <requiredEntity type="enabled">EnableRMAStorefront</requiredEntity> - </entity> - <entity name="EnableRMAStorefront" type="enabled"> - <data key="value">1</data> - </entity> - - <entity name="DisableRMA" type="sales_rma_config"> - <requiredEntity type="enabled">DisableRMAStorefront</requiredEntity> - </entity> - <entity name="DisableRMAStorefront" type="enabled"> - <data key="value">0</data> - </entity> -</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/sales_enable_rma_config-meta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/sales_enable_rma_config-meta.xml deleted file mode 100644 index 86226265dd146..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Metadata/sales_enable_rma_config-meta.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?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="SalesRMAConfig" dataType="sales_rma_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST"> - <object key="groups" dataType="sales_rma_config"> - <object key="rma" dataType="sales_rma_config"> - <object key="fields" dataType="sales_rma_config"> - <object key="enabled" dataType="enabled"> - <field key="value">string</field> - </object> - </object> - </object> - </object> - </operation> -</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderPage.xml new file mode 100644 index 0000000000000..6abe265a37b79 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderPage" url="sales/order/view/order_id/{{var1}}" area="admin" module="Magento_Sales" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml new file mode 100644 index 0000000000000..2041bf8f3c9ae --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderProcessDataPage" url="sales/order_create/processData" area="admin" module="Magento_Sales"> + <section name="AdminOrderFormItemsOrderedSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml new file mode 100644 index 0000000000000..b158e4923074a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderStatusPage" url="sales/order_status" area="admin" module="Magento_Sales"> + <section name="AdminOrderStatusFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml deleted file mode 100644 index 2a14f814eac16..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="StorefrontCreateNewReturnPage" url="rma/guest/create/order_id/" area="guest" module="Magento_Sales"> - <section name="StorefrontCreateNewReturnMainSection"/> - </page> -</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml index 32d94c3175807..ee546174d9680 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml @@ -9,7 +9,6 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontOrdersAndReturnsPage" url="sales/guest/form" area="guest" module="Magento_Sales"> - <section name="OrderAndReturnsMainSection"/> - <section name="OrderInformationSection"/> + <section name="StorefrontOrderAndReturnInformationSection"/> </page> </pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml index d65074b3eb1e3..731c529f2aec0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -21,5 +21,6 @@ <element name="submitRefundOffline" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-button']" timeout="30"/> <element name="creditMemoItem" type="text" selector="#sales_order_view_tabs_order_creditmemos"/> <element name="viewMemo" type="text" selector="div#sales_order_view_tabs_order_creditmemos_content a.action-menu-item"/> + <element name="refundOffline" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-offline']"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml index bc7fc8145af33..011500fac3f69 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml @@ -9,7 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminInvoiceMainActionsSection"> - <element name="submitInvoice" type="button" selector=".action-default.scalable.save.submit-button.primary"/> + <element name="submitInvoice" type="button" selector=".action-default.scalable.save.submit-button.primary" timeout="60"/> <element name="openNewCreditMemoFromInvoice" type="button" selector=".action-default.scalable.credit-memo"/> <element name="submitNewRefundFromInvoice" type="button" selector=".action-default.scalable.save.submit-button refund primary"/> </section> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml index 578022217f358..6fa5d9a9a3787 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml @@ -18,5 +18,6 @@ <element name="ship" type="button" selector="#order_ship" timeout="30"/> <element name="reorder" type="button" selector="#order_reorder" timeout="30"/> <element name="edit" type="button" selector="#order_edit" timeout="30"/> + <element name="modalOk" type="button" selector=".action-accept"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml index 2f6149dfa1cb7..027962282b2c3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml @@ -12,5 +12,8 @@ <element name="SubmitOrder" type="button" selector="#submit_order_top_button" timeout="30"/> <element name="Cancel" type="button" selector="#reset_order_top_button" timeout="30"/> <element name="CreateNewCustomer" type="button" selector="#order-customer-selector .actions button.primary" timeout="30"/> + <element name="submitOrder" type="button" selector="#submit_order_top_button" timeout="30"/> + <element name="cancel" type="button" selector="#reset_order_top_button" timeout="30"/> + <element name="createNewCustomer" type="button" selector="#order-customer-selector .actions button.primary" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml index 11673f1f0fe26..beb566b20806c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml @@ -15,5 +15,6 @@ <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="itemsOrderedSummaryText" type="textarea" selector="//table[@class='data-table admin__table-primary order-tables']/tfoot/tr"/> + <element name="configureSelectAttribute" type="select" selector="select[id*=attribute]"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index 22bff9c286d0f..1a12a68a6874a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -16,5 +16,6 @@ <element name="freeShippingOption" type="radio" selector="#s_method_freeshipping_freeshipping" timeout="30"/> <element name="checkMoneyOption" type="radio" selector="#p_method_checkmo" timeout="30"/> <element name="paymentBlock" type="text" selector="#order-billing_method" /> + <element name="paymentError" type="text" selector="#payment[method]-error"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml index b79d933268769..0f1461b121e15 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormShippingAddressSection"> <element name="SameAsBilling" type="checkbox" selector="#order-shipping_same_as_billing"/> + <element name="SelectFromExistingCustomerAddress" type="select" selector="#order-shipping_address_customer_address_id"/> <element name="NamePrefix" type="input" selector="#order-shipping_address_prefix"/> <element name="FirstName" type="input" selector="#order-shipping_address_firstname"/> <element name="MiddleName" type="input" selector="#order-shipping_address_middlename"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml index 53aeeb62c6b70..5c2ff296ebeee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml @@ -23,8 +23,10 @@ <element name="productNameColumn" type="text" selector=".edit-order-table .col-product .product-title"/> <element name="productNameOptions" type="text" selector=".edit-order-table .col-product .item-options"/> + <element name="productName" type="text" selector="#order-items_grid span[id*=order_item]"/> <element name="productNameOptionsLink" type="text" selector="//table[contains(@class, 'edit-order-table')]//td[contains(@class, 'col-product')]//a[text() = '{{var1}}']" parameterized="true"/> <element name="productSkuColumn" type="text" selector=".edit-order-table .col-product .product-sku-block"/> + <element name="productTotal" type="text" selector="#order-items_grid .col-total"/> <element name="statusColumn" type="text" selector=".edit-order-table .col-status"/> <element name="originalPriceColumn" type="text" selector=".edit-order-table .col-original-price .price"/> <element name="priceColumn" type="text" selector=".edit-order-table .col-price .price"/> @@ -35,4 +37,4 @@ <element name="discountAmountColumn" type="text" selector=".edit-order-table .col-discont .price"/> <element name="totalColumn" type="text" selector=".edit-order-table .col-total .price"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml new file mode 100644 index 0000000000000..1058b2d6f2177 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.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="AdminOrderStatusFormSection"> + <element name="statusCodeField" type="text" selector="#edit_form [name=status]"/> + <element name="statusLabelField" type="text" selector="#edit_form [name=label]"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml new file mode 100644 index 0000000000000..b624639281187 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderStatusGridSection"> + <element name="statusCodeFilterField" type="input" selector="[data-role=filter-form] [name=status]"/> + <element name="statusCodeDataColumn" type="input" selector="[data-role=row] [data-column=status]"/> + <element name="statusLabelDataColumn" type="input" selector="[data-role=row] [data-column=label]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/ConfigurationListSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/ConfigurationListSection.xml new file mode 100644 index 0000000000000..bce5f95cf78a6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/ConfigurationListSection.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="ConfigurationListSection"> + <element name="sales" type="button" selector="//div[contains(@class, 'admin__page-nav-title title _collapsible')]/strong[text()='Sales']"/> + <element name="salesPaymentMethods" type="button" selector="//span[text()='Payment Methods']"/> + </section> +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/NewOrderSection.xml similarity index 89% rename from app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml rename to app/code/Magento/Sales/Test/Mftf/Section/NewOrderSection.xml index 13f59ad2cf18e..26e00bf4c0aa4 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/NewOrderSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewOrderSection"> <element name="createNewOrder" type="button" selector="#add"/> <element name="customer" type="button" selector="//td[contains(text(), 'Abgar')]"/> @@ -30,6 +32,5 @@ <element name="cvv" type="input" selector="#cvv"/> <element name="submitOrder" type="input" selector="#submit_order_top_button"/> <element name="successMessage" type="input" selector="#messages"/> - </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml index 717022322698f..b716047a39008 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml @@ -19,7 +19,7 @@ <element name="website" type="radio" selector="//label[contains(text(), '{{arg}}')]" parameterized="true"/> <element name="addProducts" type="button" selector="#add_products"/> - <element name="selectProduct" type="checkbox" selector="//td[contains(text(), '{{arg}}')]/following-sibling::td[contains(@class, 'col-select col-in_products')]" parameterized="true"/> + <element name="selectProduct" type="checkbox" selector="//td[contains(text(), '{{arg}}')]/following-sibling::td[contains(@class, 'col-select col-in_products')]/label/input" parameterized="true"/> <element name="setQuantity" type="checkbox" selector="//td[contains(text(), '{{arg}}')]/following-sibling::td[contains(@class, 'col-qty')]/input" parameterized="true"/> <element name="addProductsToOrder" type="button" selector="//span[text()='Add Selected Product(s) to Order']"/> <element name="customPrice" type="checkbox" selector="//span[text()='{{arg}}']/parent::td/following-sibling::td/div//span[contains(text(),'Custom Price')]" parameterized="true"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCreateNewReturnMainSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCreateNewReturnMainSection.xml deleted file mode 100644 index fe8391cf3c28f..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCreateNewReturnMainSection.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?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="StorefrontCreateNewReturnMainSection"> - <element name="quantityToReturn" type="input" selector="#items:qty_requested0"/> - <element name="submit" type="submit" selector="//span[contains(text(), 'Submit')]"/> - <element name="resolutionError" type="text" selector="//*[@id='items:resolution0']/following-sibling::div[contains(text(),'Please select an option')]"/> - <element name="conditionError" type="text" selector="//*[@id='items:condition0']/following-sibling::div[contains(text(),'Please select an option')]"/> - <element name="reasonError" type="text" selector="//*[@id='items:reason0']/following-sibling::div[contains(text(),'Please select an option')]"/> - </section> -</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 9180636db7821..e405173429b2c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -25,6 +25,7 @@ </createData> <!-- Enable *Free Shipping* --> <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> @@ -33,62 +34,53 @@ <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <actionGroup ref="logout" stepKey="logOut"/> </after> - <!-- Flush Magento Cache --> - <magentoCLI stepKey="flushCache" command="cache:flush"/> - <!--Proceed to Admin panel > SALES > Orders. Created order should be in Processing status--> - <amOnPage url="{{AdminOrderCreatePage.url}}" stepKey="navigateToSalesOrderPage"/> - <waitForPageLoad stepKey="waitForSalesOrderPageLoaded"/> - - <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> - <waitForElementVisible stepKey="waitForNewOrderPageOpened" selector="{{NewOrderSection.submitOrder}}"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + <actionGroup ref="navigateToNewOrderPageNewCustomer" stepKey="navigateToNewOrderPage"/> <!--Check if order can be submitted without the required fields including email address--> - <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="seeNewOrderPageTitle"/> - <actionGroup ref="addSimpleProductToOrder" stepKey="addFirstProductToOrder" after="scrollToTopOfOrderFormPage"> + <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addFirstProductToOrder"> <argument name="product" value="$$createProduct$$"/> </actionGroup> <!--Click *Custom Price* link, enter 0 and click *Update Items and Quantities* button--> <click selector="{{AdminOrderFormItemsSection.customPriceCheckbox}}" stepKey="clickCustomPriceCheckbox"/> - <waitForElementVisible stepKey="waitForPriceFieldAppears" selector="{{AdminOrderFormItemsSection.customPriceField}}"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.customPriceField}}" stepKey="waitForPriceFieldAppears"/> <fillField selector="{{AdminOrderFormItemsSection.customPriceField}}" userInput="0" stepKey="fillCustomPriceField"/> <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="clickUpdateItemsAndQuantitiesButton"/> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="clickUpdateItemsAndQuantitiesButton"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> <!--Fill customer address information--> - <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="fillCustomerEmail"> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> <argument name="customer" value="Simple_US_Customer"/> <argument name="address" value="US_Address_TX"/> </actionGroup> <!-- Select Free shipping --> - <actionGroup ref="orderSelectFreeShipping" stepKey="selectFreeShippingOption" after="fillCustomerAddress"/> + <actionGroup ref="orderSelectFreeShipping" stepKey="selectFreeShippingOption"/> <!--Click *Submit Order* button--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder" after="selectFreeShippingOption"/> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> <!--Click *Invoice* button--> - <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> - <waitForPageLoad stepKey="waitForInvoicePageOpened"/> - - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForInvoiceSaved"/> - <see userInput="The invoice has been created." stepKey="seeCorrectMessage"/> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> <!--Verify that *Credit Memo* button is displayed--> <seeElement selector="{{AdminOrderFormItemsSection.creditMemo}}" stepKey="seeCreditMemo"/> <click selector="{{AdminOrderFormItemsSection.creditMemo}}" stepKey="clickCreditMemoItem"/> <waitForPageLoad stepKey="waitForCreditMemoPageLoaded"/> - <see stepKey="seeNewMemoPage" userInput="New Memo"/> - <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeUrlOnPage"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeNewMemoUrlOnPage"/> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml new file mode 100644 index 0000000000000..85ef563e10db7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml @@ -0,0 +1,38 @@ +<?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="AdminChangeCustomerGroupInNewOrder"> + <annotations> + <title value="Customer account group cannot be selected while creating a new customer in order"/> + <stories value="MC-15290: Customer account group cannot be selected while creating a new customer in order"/> + <description value="Customer account group cannot be selected while creating a new customer in order"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15290"/> + <useCaseId value="MC-15289"/> + <group value="sales"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="openNewOrder"/> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="Retailer" stepKey="selectCustomerGroup"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <grabValueFrom selector="{{AdminOrderFormAccountSection.group}}" stepKey="grabGroupValue"/> + <assertEquals stepKey="assertValueIsStillSelected"> + <actualResult type="variable">$grabGroupValue</actualResult> + <expectedResult type="string">3</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml new file mode 100644 index 0000000000000..f869841153aea --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -0,0 +1,91 @@ +<?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="AdminCorrectnessInvoicedItemInBundleProductTest"> + <annotations> + <features value="Sales"/> + <title value="Check correctness of invoiced items in a Bundle Product"/> + <description value="Check correctness of invoiced items in a Bundle Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11059"/> + <useCaseId value="MC-10969"/> + <group value="sales"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category and simple product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + <field key="qty">10</field> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--Complete Bundle product creation--> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to bundle product page--> + <amOnPage url="{{StorefrontProductPage.url($$createCategory.name$$)}}" stepKey="navigateToBundleProductPage"/> + + <!--Place order bundle product with 10 options--> + <actionGroup ref="StorefrontAddCategoryBundleProductToCartActionGroup" stepKey="addBundleProductToCart"> + <argument name="product" value="$$createBundleProduct$$"/> + <argument name="quantity" value="10"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <!--Go to order page submit invoice--> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForCreatedOrderPageOpened"/> + <actionGroup ref="goToInvoiceIntoOrder" stepKey="goToInvoiceIntoOrderPage"/> + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="5" stepKey="ChangeQtyToInvoice"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQunatity"/> + <waitForPageLoad stepKey="waitPageToBeLoaded"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Verify invoiced items qty in ship tab--> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipment"/> + <grabTextFrom selector="{{AdminShipmentItemsSection.itemQtyInvoiced('1')}}" stepKey="grabInvoicedItemQty"/> + <assertEquals expected="5" expectedType="string" actual="$grabInvoicedItemQty" stepKey="assertInvoicedItemsQty"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index 94e99d25dbb60..ce66409ed9b3c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminCreateInvoiceTest"> <annotations> <features value="Sales"/> @@ -71,7 +71,6 @@ <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForInvoicePageLoad"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoices"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml new file mode 100644 index 0000000000000..40a731410a899 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml @@ -0,0 +1,39 @@ +<?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="AdminCreateOrderStatusDuplicatingCodeTest"> + <annotations> + <stories value="Create order status"/> + <title value="Create order status with duplicating code"/> + <description value="Receive error when creating order status with the code which is already exist"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15432" /> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{duplicatingCodeOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingCodeOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveDuplicateError" stepKey="seeFormSaveDuplicateError"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml new file mode 100644 index 0000000000000..d1381bbb1efb0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.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="AdminCreateOrderStatusDuplicatingLabelTest"> + <annotations> + <stories value="Create order status"/> + <title value="Create order status with duplicating label"/> + <description value="Create an order status and get success message"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15433" /> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{duplicatingLabelOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingLabelOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> + + <!-- Verify the order status grid page shows the order status we just created --> + <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> + <argument name="status" value="{{duplicatingLabelOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingLabelOrderStatus.label}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml new file mode 100644 index 0000000000000..c2daaac84dd42 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.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="AdminCreateOrderStatusTest"> + <annotations> + <stories value="Create custom order status"/> + <title value="Create custom order status"/> + <description value="Tests opening admin order status page, create a new order status with success message"/> + <testCaseId value="MC-15431" /> + <severity value="AVERAGE"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> + + <!-- Verify the order status grid page shows the order status we just created --> + <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml index f15f5de5df696..d087b291de87c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminCreateOrderWithBundleProductTest"> <annotations> <title value="Create Order in Admin and update bundle product configuration"/> @@ -108,4 +109,4 @@ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> </after> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml new file mode 100644 index 0000000000000..af7cc1822d215 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSalesCreditMemosNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales credit memos navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Credit Memos"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14140"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesCreditMemosPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesCreditMemos.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesCreditMemos.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml new file mode 100644 index 0000000000000..5a38a66d1f4b2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSalesInvoicesNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales invoices navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Invoices"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14138"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesInvoicesPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesInvoices.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesInvoices.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml new file mode 100644 index 0000000000000..8099254923a2c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSalesOrdersNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales orders navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Orders"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14137"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesOrderPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesOrders.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesOrders.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml new file mode 100644 index 0000000000000..5717c6c90fc17 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSalesShipmentsNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales shipments navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Shipments"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14139"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesShipmentsPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesShipments.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesShipments.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml new file mode 100644 index 0000000000000..68933be92efe6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSalesTransactionsNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales transactions navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Transactions"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14141"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesTransactionsPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesTransactions.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesTransactions.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml new file mode 100644 index 0000000000000..d55cde1449033 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresOrderStatusNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin stores order status navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Order Status"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14142"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresOrderStatusPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsOrderStatus.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsOrderStatus.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml index 041252af0ac5b..63607e59c41b2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminSubmitConfigurableProductOrderTest"> <annotations> <title value="Create Order in Admin and update product configuration"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml new file mode 100644 index 0000000000000..e487c62b96727 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.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="AdminSubmitsOrderPaymentMethodValidationTest"> + <annotations> + <features value="Sales"/> + <stories value="MC-5537: No UI validation for Payment methods when creating an order from admin"/> + <title value="UI validation for Payment methods when creating an order from admin"/> + <description value="Admin should not be able to submit orders without selecting a payment method when there is more than one"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-6029"/> + <group value="sales"/> + </annotations> + <before> + <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 1" /> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 0" /> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + <!--Create order via Admin--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + + <!--Check if order can be submitted without the required fields--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder" after="seeNewOrderPageTitle"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="checkRequiredFieldsNewOrderForm" stepKey="checkRequiredFieldsNewOrder" after="addSimpleProductToOrder"/> + <see selector="{{AdminOrderFormPaymentSection.paymentError}}" userInput="Please select one of the options." stepKey="seePaymentMethodRequired" after="checkRequiredFieldsNewOrder"/> + <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"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="fillCustomerEmail"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select payment and shipping --> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" stepKey="waitForPaymentOptions"/> + <selectOption selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" userInput="checkmo" stepKey="checkPaymentOption"/> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" after="fillCustomerAddress"/> + + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal" after="selectFlatRateShipping"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping" after="seeOrderSubTotal"/> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" stepKey="seeCorrectGrandTotal" after="scrollToOrderGrandTotal"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder" after="seeCorrectGrandTotal"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml new file mode 100644 index 0000000000000..d418751c736e1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -0,0 +1,88 @@ +<?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="AdminSubmitsOrderWithAndWithoutFieldsValidationTest"> + <annotations> + <features value="Sales"/> + <stories value="Create orders"/> + <title value="Fields validation is required to create an order from Admin Panel"/> + <description value="Admin should not be able to submit orders without invalid address fields"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + <!--Create order via Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <!--<actionGroup ref="navigateToNewOrderPageNewCustomer" stepKey="navigateToNewOrderPage"/>--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + + <!--Check if order can be submitted without the required fields including email address--> + <actionGroup ref="checkRequiredFieldsNewOrderForm" stepKey="checkRequiredFieldsNewOrder" after="seeNewOrderPageTitle"/> + <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="checkRequiredFieldsNewOrder"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder" after="scrollToTopOfOrderFormPage"> + <argument name="product" value="_defaultProduct"/> + </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"/> + + <!--Fill wrong customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillWrongCustomerAddress" after="fillCustomerEmail"> + <argument name="customer" value="Simple_US_Customer_Incorrect_Name"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" + after="fillWrongCustomerAddress"/> + + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal" after="selectFlatRateShipping"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping" after="seeOrderSubTotal"/> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" stepKey="seeCorrectGrandTotal" after="scrollToOrderGrandTotal"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrderWrong" + after="seeCorrectGrandTotal"/> + <see selector="{{AdminOrderFormBillingAddressSection.firstNameError}}" + userInput="Please enter less or equal than 255 symbols." stepKey="firstNameError" + after="clickSubmitOrderWrong"/> + + <!--Fill correct customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="firstNameError"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select shipping --> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShipping" after="fillCustomerAddress"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="selectShipping" after="clickShipping"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder" after="selectShipping"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml index 9790b5dfc47f3..ad3a411d92414 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontRedirectToOrderHistory"> <annotations> <features value="Redirection Rules"/> 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 4ad2e314c8317..c3ff8a2acaf4f 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 @@ -203,10 +203,9 @@ public function testSaveActionWithNegativeCreditmemo() $creditmemoMock = $this->createPartialMock( \Magento\Sales\Model\Order\Creditmemo::class, - ['load', 'getGrandTotal', 'isAllowZeroGrandTotal', '__wakeup'] + ['load', 'isValidGrandTotal', '__wakeup'] ); - $creditmemoMock->expects($this->once())->method('getGrandTotal')->will($this->returnValue('0')); - $creditmemoMock->expects($this->once())->method('isAllowZeroGrandTotal')->will($this->returnValue(false)); + $creditmemoMock->expects($this->once())->method('isValidGrandTotal')->will($this->returnValue(false)); $this->memoLoaderMock->expects( $this->once() )->method( diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php index 05c99c9f9ef98..87e27fdb2206b 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php @@ -6,12 +6,14 @@ namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\OrderRepositoryInterface; /** * Class NewActionTest * @package Magento\Sales\Controller\Adminhtml\Order\Invoice * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class NewActionTest extends \PHPUnit\Framework\TestCase { @@ -90,6 +92,11 @@ class NewActionTest extends \PHPUnit\Framework\TestCase */ protected $invoiceServiceMock; + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -215,12 +222,15 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); + $this->controller = $objectManager->getObject( \Magento\Sales\Controller\Adminhtml\Order\Invoice\NewAction::class, [ 'context' => $contextMock, 'resultPageFactory' => $this->resultPageFactoryMock, - 'invoiceService' => $this->invoiceServiceMock + 'invoiceService' => $this->invoiceServiceMock, + 'orderRepository' => $this->orderRepositoryMock ] ); } @@ -250,19 +260,17 @@ public function testExecute() $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getId', 'canInvoice']) + ->setMethods(['load', 'canInvoice']) ->getMock(); - $orderMock->expects($this->once()) - ->method('load') - ->with($orderId) - ->willReturnSelf(); - $orderMock->expects($this->once()) - ->method('getId') - ->willReturn($orderId); $orderMock->expects($this->once()) ->method('canInvoice') ->willReturn(true); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + $this->invoiceServiceMock->expects($this->once()) ->method('prepareInvoice') ->with($orderMock, []) @@ -285,11 +293,7 @@ public function testExecute() ->with(true) ->will($this->returnValue($commentText)); - $this->objectManagerMock->expects($this->at(0)) - ->method('create') - ->with(\Magento\Sales\Model\Order::class) - ->willReturn($orderMock); - $this->objectManagerMock->expects($this->at(1)) + $this->objectManagerMock->expects($this->once()) ->method('get') ->with(\Magento\Backend\Model\Session::class) ->will($this->returnValue($this->sessionMock)); @@ -318,19 +322,12 @@ public function testExecuteNoOrder() $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getId', 'canInvoice']) + ->setMethods(['canInvoice']) ->getMock(); - $orderMock->expects($this->once()) - ->method('load') - ->with($orderId) - ->willReturnSelf(); - $orderMock->expects($this->once()) - ->method('getId') - ->willReturn(null); - $this->objectManagerMock->expects($this->at(0)) - ->method('create') - ->with(\Magento\Sales\Model\Order::class) + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) ->willReturn($orderMock); $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 9fd2a8b0d929f..467476c9bb406 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -1,5 +1,4 @@ <?php - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -249,7 +248,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -279,7 +278,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php index f299cd5accdb8..4d8dd00ac65b3 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php @@ -117,27 +117,9 @@ public function testIsValidGrandTotalGrandTotalEmpty() public function testIsValidGrandTotalGrandTotal() { $this->creditmemo->setGrandTotal(0); - $this->creditmemo->isAllowZeroGrandTotal(true); $this->assertFalse($this->creditmemo->isValidGrandTotal()); } - /** - * Test for isAllowZeroGrandTotal method. - * - * @return void - */ - public function testIsAllowZeroGrandTotal() - { - $isAllowed = 0; - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with( - 'sales/zerograndtotal_creditmemo/allow_zero_grandtotal', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - )->willReturn($isAllowed); - $this->assertEquals($isAllowed, $this->creditmemo->isAllowZeroGrandTotal()); - } - public function testIsValidGrandTotal() { $this->creditmemo->setGrandTotal(1); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php index 31bf846689230..1f074d7262f4d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -7,6 +7,9 @@ use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; +/** + * Test for Magento\Sales\Model\Order\Email\Sender\CreditmemoSender class. + */ class CreditmemoSenderTest extends AbstractSenderTest { /** @@ -90,7 +93,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -130,7 +133,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -197,6 +200,8 @@ public function sendDataProvider() * @param bool $isVirtualOrder * @param int $formatCallCount * @param string|null $expectedShippingAddress + * + * @return void * @dataProvider sendVirtualOrderDataProvider */ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expectedShippingAddress) @@ -207,7 +212,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -242,7 +247,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php index 9c54c716e4207..d1aa5af53da4d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -7,6 +7,9 @@ use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +/** + * Test for Magento\Sales\Model\Order\Email\Sender\InvoiceSender class. + */ class InvoiceSenderTest extends AbstractSenderTest { /** @@ -90,7 +93,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -136,7 +139,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -212,7 +215,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -247,7 +250,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index 46c44c03b1514..88053ea684ce8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -64,7 +64,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -72,7 +72,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen ->willReturn($configValue); if (!$configValue || $forceSyncMode) { - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -118,7 +118,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setEmailSent') - ->with(true); + ->with($emailSendingResult); $this->orderResourceMock->expects($this->once()) ->method('saveAttribute') @@ -210,7 +210,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->with('sales_email/general/async_sending') ->willReturn(false); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(true); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php index b1b18af63b590..2d7b42bccae5a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -7,6 +7,9 @@ use Magento\Sales\Model\Order\Email\Sender\ShipmentSender; +/** + * Test for Magento\Sales\Model\Order\Email\Sender\ShipmentSender class. + */ class ShipmentSenderTest extends AbstractSenderTest { /** @@ -90,7 +93,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -136,7 +139,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -212,7 +215,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -247,7 +250,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); $this->shipmentResourceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index 759d60d9e6613..24cd54e3a46b3 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php @@ -76,7 +76,7 @@ protected function setUp() 'setTemplateIdentifier', 'setTemplateOptions', 'setTemplateVars', - 'setFromByStore', + 'setFromByScope', ] ); @@ -103,7 +103,7 @@ protected function setUp() ->method('getEmailIdentity') ->will($this->returnValue($emailIdentity)); $this->transportBuilder->expects($this->once()) - ->method('setFromByStore') + ->method('setFromByScope') ->with($this->equalTo($emailIdentity), 1); $this->identityContainerMock->expects($this->once()) @@ -146,7 +146,7 @@ public function testSend() ->method('getId') ->willReturn(1); $this->transportBuilder->expects($this->once()) - ->method('setFromByStore') + ->method('setFromByScope') ->with($identity, 1); $this->transportBuilder->expects($this->once()) ->method('addTo') @@ -176,7 +176,7 @@ public function testSendCopyTo() ->method('addTo') ->with($this->equalTo('example@mail.com')); $this->transportBuilder->expects($this->once()) - ->method('setFromByStore') + ->method('setFromByScope') ->with($identity, 1); $this->identityContainerMock->expects($this->once()) ->method('getStore') diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index 8a4e2920ba207..dcf689cf7d53b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -247,7 +247,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -277,7 +277,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php index 30b584b8c4ebf..9d0f10a30e6ef 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model\Order; use Magento\Framework\Model\Context; @@ -1526,7 +1527,7 @@ public function testRefund() $this->orderStateResolver->expects($this->once())->method('getStateForOrder') ->with($this->order) ->willReturn(Order::STATE_CLOSED); - $this->mockGetDefaultStatus(Order::STATE_CLOSED, $status); + $this->mockGetDefaultStatus(Order::STATE_CLOSED, $status, ['first, second']); $this->assertOrderUpdated(Order::STATE_PROCESSING, $status, $message); static::assertSame($this->payment, $this->payment->refund($this->creditMemoMock)); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 94347e8b32d54..391e99ba6f835 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -249,7 +249,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -279,7 +279,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php index 2e82d8064a9e8..7f0c0639d21f5 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php @@ -10,6 +10,7 @@ use Magento\Sales\Api\Data\OrderSearchResultInterfaceFactory as SearchResultFactory; use Magento\Sales\Model\ResourceModel\Metadata; use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -46,6 +47,11 @@ class OrderRepositoryTest extends \PHPUnit\Framework\TestCase */ private $orderTaxManagementMock; + /** + * @var PaymentAdditionalInfoInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentAdditionalInfoFactory; + /** * Setup the test * @@ -69,6 +75,8 @@ protected function setUp() $this->orderTaxManagementMock = $this->getMockBuilder(OrderTaxManagementInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->paymentAdditionalInfoFactory = $this->getMockBuilder(PaymentAdditionalInfoInterfaceFactory::class) + ->disableOriginalConstructor()->setMethods(['create'])->getMockForAbstractClass(); $this->orderRepository = $this->objectManager->getObject( \Magento\Sales\Model\OrderRepository::class, [ @@ -76,7 +84,8 @@ protected function setUp() 'searchResultFactory' => $this->searchResultFactory, 'collectionProcessor' => $this->collectionProcessor, 'orderExtensionFactory' => $orderExtensionFactoryMock, - 'orderTaxManagement' => $this->orderTaxManagementMock + 'orderTaxManagement' => $this->orderTaxManagementMock, + 'paymentAdditionalInfoFactory' => $this->paymentAdditionalInfoFactory ] ); } @@ -95,12 +104,16 @@ public function testGetList() $orderTaxDetailsMock = $this->getMockBuilder(\Magento\Tax\Api\Data\OrderTaxDetailsInterface::class) ->disableOriginalConstructor() ->setMethods(['getAppliedTaxes', 'getItems'])->getMockForAbstractClass(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderPaymentInterface::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $paymentAdditionalInfo = $this->getMockBuilder(\Magento\Payment\Api\Data\PaymentAdditionalInfoInterface::class) + ->disableOriginalConstructor()->setMethods(['setKey', 'setValue'])->getMockForAbstractClass(); $extensionAttributes = $this->createPartialMock( \Magento\Sales\Api\Data\OrderExtension::class, [ 'getShippingAssignments', 'setShippingAssignments', 'setConvertingFromQuote', - 'setAppliedTaxes', 'setItemAppliedTaxes' + 'setAppliedTaxes', 'setItemAppliedTaxes', 'setPaymentAdditionalInfo' ] ); $shippingAssignmentBuilder = $this->createMock( @@ -111,6 +124,13 @@ public function testGetList() ->method('process') ->with($searchCriteriaMock, $collectionMock); $itemsMock->expects($this->atLeastOnce())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atleastOnce())->method('getPayment')->willReturn($paymentMock); + $paymentMock->expects($this->atLeastOnce())->method('getAdditionalInformation') + ->willReturn(['method' => 'checkmo']); + $this->paymentAdditionalInfoFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($paymentAdditionalInfo); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setKey')->willReturnSelf(); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setValue')->willReturnSelf(); $this->orderTaxManagementMock->expects($this->atLeastOnce())->method('getOrderTaxDetails') ->willReturn($orderTaxDetailsMock); $extensionAttributes->expects($this->any()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index f724136eb5154..705d2face2308 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -11,7 +11,12 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order; +use Magento\Sales\Model\ResourceModel\Order\Item\Collection; use Magento\Sales\Model\ResourceModel\Order\Status\History\CollectionFactory as HistoryCollectionFactory; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Sales\Api\Data\OrderItemSearchResultInterface; /** * Test class for \Magento\Sales\Model\Order @@ -87,6 +92,16 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ private $timezone; + /** + * @var OrderItemRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemRepository; + + /** + * @var SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchCriteriaBuilder; + protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -144,6 +159,15 @@ protected function setUp() $this->eventManager = $this->createMock(\Magento\Framework\Event\Manager::class); $context = $this->createPartialMock(\Magento\Framework\Model\Context::class, ['getEventDispatcher']); $context->expects($this->any())->method('getEventDispatcher')->willReturn($this->eventManager); + + $this->itemRepository = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->setMethods(['getList']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + + $this->searchCriteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->setMethods(['addFilter', 'create']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->order = $helper->getObject( \Magento\Sales\Model\Order::class, [ @@ -157,37 +181,80 @@ protected function setUp() 'productListFactory' => $this->productCollectionFactoryMock, 'localeResolver' => $this->localeResolver, 'timezone' => $this->timezone, + 'itemRepository' => $this->itemRepository, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder ] ); } - public function testGetItemById() + /** + * Test testGetItems method. + */ + public function testGetItems() { - $realOrderItemId = 1; - $fakeOrderItemId = 2; + $orderItems = [$this->item]; - $orderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $this->searchCriteriaBuilder->expects($this->once())->method('addFilter')->willReturnSelf(); + + $searchCriteria = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->searchCriteriaBuilder->expects($this->once())->method('create')->willReturn($searchCriteria); + + $itemsCollection = $this->getMockBuilder(OrderItemSearchResultInterface::class) + ->setMethods(['getItems']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $itemsCollection->expects($this->once())->method('getItems')->willReturn($orderItems); + $this->itemRepository->expects($this->once())->method('getList')->willReturn($itemsCollection); + + $this->assertEquals($orderItems, $this->order->getItems()); + } + /** + * Prepare order item mock. + * + * @param int $orderId + * @return void + */ + private function prepareOrderItem(int $orderId = 0) + { $this->order->setData( \Magento\Sales\Api\Data\OrderInterface::ITEMS, [ - $realOrderItemId => $orderItem + $orderId => $this->item ] ); + } + + /** + * Test GetItemById method. + * + * @return void + */ + public function testGetItemById() + { + $realOrderItemId = 1; + $fakeOrderItemId = 2; + + $this->prepareOrderItem($realOrderItemId); - $this->assertEquals($orderItem, $this->order->getItemById($realOrderItemId)); + $this->assertEquals($this->item, $this->order->getItemById($realOrderItemId)); $this->assertEquals(null, $this->order->getItemById($fakeOrderItemId)); } /** + * Test GetItemByQuoteItemId method. + * * @param int|null $gettingQuoteItemId * @param int|null $quoteItemId * @param string|null $result * * @dataProvider dataProviderGetItemByQuoteItemId + * @return void */ public function testGetItemByQuoteItemId($gettingQuoteItemId, $quoteItemId, $result) { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQuoteItemId') ->willReturn($gettingQuoteItemId); @@ -212,14 +279,19 @@ public function dataProviderGetItemByQuoteItemId() } /** + * Test getAllVisibleItems method. + * * @param bool $isDeleted * @param int|null $parentItemId * @param array $result * * @dataProvider dataProviderGetAllVisibleItems + * @return void */ public function testGetAllVisibleItems($isDeleted, $parentItemId, array $result) { + $this->prepareOrderItem(); + $this->item->expects($this->once()) ->method('isDeleted') ->willReturn($isDeleted); @@ -263,8 +335,15 @@ public function testCanCancelIsPaymentReview() $this->assertFalse($this->order->canCancel()); } + /** + * Test CanInvoice method. + * + * @return void + */ public function testCanInvoice() { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(42); @@ -304,8 +383,15 @@ public function testCanNotInvoiceWhenActionInvoiceFlagIsFalse() $this->assertFalse($this->order->canInvoice()); } + /** + * Test CanNotInvoice method when invoice is locked. + * + * @return void + */ public function testCanNotInvoiceWhenLockedInvoice() { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(42); @@ -315,8 +401,15 @@ public function testCanNotInvoiceWhenLockedInvoice() $this->assertFalse($this->order->canInvoice()); } + /** + * Test CanNotInvoice method when didn't have qty to invoice. + * + * @return void + */ public function testCanNotInvoiceWhenDidNotHaveQtyToInvoice() { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(0); @@ -329,29 +422,16 @@ public function testCanNotInvoiceWhenDidNotHaveQtyToInvoice() public function testCanCreditMemo() { $totalPaid = 10; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->priceCurrency->expects($this->once())->method('round')->with($totalPaid)->willReturnArgument(0); $this->assertTrue($this->order->canCreditmemo()); } - /** - * Test canCreditMemo method when grand total and paid total are zero. - * - * @return void - */ - public function testCanCreditMemoForZeroTotal() - { - $grandTotal = 0; - $totalPaid = 0; - $totalRefunded = 0; - $this->order->setGrandTotal($grandTotal); - $this->order->setTotalPaid($totalPaid); - $this->assertFalse($this->order->canCreditmemoForZeroTotal($totalRefunded)); - } - public function testCanNotCreditMemoWithTotalNull() { $totalPaid = 0; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->priceCurrency->expects($this->once())->method('round')->with($totalPaid)->willReturnArgument(0); $this->assertFalse($this->order->canCreditmemo()); @@ -363,6 +443,7 @@ public function testCanNotCreditMemoWithAdjustmentNegative() $adjustmentNegative = 10; $totalRefunded = 90; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->order->setTotalRefunded($totalRefunded); $this->order->setAdjustmentNegative($adjustmentNegative); @@ -377,6 +458,7 @@ public function testCanCreditMemoWithAdjustmentNegativeLowerThanTotalPaid() $adjustmentNegative = 9; $totalRefunded = 90; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->order->setTotalRefunded($totalRefunded); $this->order->setAdjustmentNegative($adjustmentNegative); @@ -601,8 +683,15 @@ public function testCanCancelCanReviewPayment() $this->assertFalse($this->order->canCancel()); } + /** + * Test CanCancelAllInvoiced method. + * + * @return void + */ public function testCanCancelAllInvoiced() { + $this->prepareOrderItem(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) ->disableOriginalConstructor() ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) @@ -662,11 +751,16 @@ public function testCanCancelState() } /** + * Test CanCancelActionFlag method. + * * @param bool $cancelActionFlag * @dataProvider dataProviderActionFlag + * @return void */ public function testCanCancelActionFlag($cancelActionFlag) { + $this->prepareOrderItem(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) ->disableOriginalConstructor() ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php index c95b56d81d6f4..1ffeaa053cc2e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model; use Magento\Framework\App\ResourceConnection; @@ -245,9 +246,9 @@ public function testOrderCreditmemo($orderId, $notify, $appendComment) ->method('setState') ->with(Order::STATE_CLOSED) ->willReturnSelf(); - $this->orderMock->expects($this->once()) - ->method('getState') - ->willReturn(Order::STATE_CLOSED); + $this->configMock->expects($this->once()) + ->method('getStateStatuses') + ->willReturn(['first, second']); $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') ->with(Order::STATE_CLOSED) diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index d21e575245395..99a411c43c247 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -107,13 +107,13 @@ public function stateCheckDataProvider() { return [ 'processing - !canCreditmemo!canShip -> closed' => - [false, 1, false, 0, Order::STATE_PROCESSING, Order::STATE_CLOSED], + [false, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_CLOSED], 'complete - !canCreditmemo,!canShip -> closed' => - [false, 1, false, 0, Order::STATE_COMPLETE, Order::STATE_CLOSED], - 'processing - !canCreditmemo,canShip -> closed' => - [false, 1, true, 0, Order::STATE_PROCESSING, Order::STATE_CLOSED], - 'complete - !canCreditmemo,canShip -> closed' => - [false, 1, true, 0, Order::STATE_COMPLETE, Order::STATE_CLOSED], + [false, 1, false, 1, Order::STATE_COMPLETE, Order::STATE_CLOSED], + 'processing - !canCreditmemo,canShip -> processing' => + [false, 1, true, 2, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - !canCreditmemo,canShip -> complete' => + [false, 1, true, 1, Order::STATE_COMPLETE, Order::STATE_COMPLETE], 'processing - canCreditmemo,!canShip -> complete' => [true, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_COMPLETE], 'complete - canCreditmemo,!canShip -> complete' => diff --git a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php index c6e02151b9bc1..e919b45667f24 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php @@ -69,6 +69,17 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) $orderMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); $this->orderRepositoryMock->expects($this->once())->method('get')->with($orderId) ->willReturn($orderMock); + + $orderMock->expects($this->once())->method('setCustomerId')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerIsGuest')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerEmail')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerFirstname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerLastname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerMiddlename')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerPrefix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerSuffix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerGroupId')->willReturn($orderMock); + if (!$customerId) { $this->orderRepositoryMock->expects($this->once())->method('save')->with($orderMock); $this->sut->execute($observerMock); diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php index 2fd792fb4ae25..48b8740d86fc0 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php @@ -44,7 +44,7 @@ public function __construct( * Prepare Data Source * * @param array $dataSource - * @return void + * @return array */ public function prepareDataSource(array $dataSource) { diff --git a/app/code/Magento/Sales/etc/adminhtml/system.xml b/app/code/Magento/Sales/etc/adminhtml/system.xml index 1b2f8b88d7dc3..2dc467d6ca247 100644 --- a/app/code/Magento/Sales/etc/adminhtml/system.xml +++ b/app/code/Magento/Sales/etc/adminhtml/system.xml @@ -89,6 +89,11 @@ <label>Minimum Amount</label> <comment>Subtotal after discount</comment> </field> + <field id="include_discount_amount" translate="label" sortOrder="12" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Include Discount Amount</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Choosing yes will be used subtotal after discount, otherwise only subtotal will be used</comment> + </field> <field id="tax_including" translate="label" sortOrder="15" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Include Tax to Amount</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> diff --git a/app/code/Magento/Sales/etc/config.xml b/app/code/Magento/Sales/etc/config.xml index 5be06fa3836a7..2480da4ad214b 100644 --- a/app/code/Magento/Sales/etc/config.xml +++ b/app/code/Magento/Sales/etc/config.xml @@ -22,6 +22,7 @@ <allow_zero_grandtotal>1</allow_zero_grandtotal> </zerograndtotal_creditmemo> <minimum_order> + <include_discount_amount>1</include_discount_amount> <tax_including>1</tax_including> </minimum_order> <orders> diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index da6d2bba552da..d6ea9b7d54861 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -22,119 +22,119 @@ comment="Store Id"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_discount_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Canceled"/> - <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Invoiced"/> - <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Refunded"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="base_shipping_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Canceled"/> - <column xsi:type="decimal" name="base_shipping_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Invoiced"/> - <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Refunded"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="base_shipping_tax_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Refunded"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="base_subtotal_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Canceled"/> - <column xsi:type="decimal" name="base_subtotal_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Invoiced"/> - <column xsi:type="decimal" name="base_subtotal_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Refunded"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="base_tax_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Canceled"/> - <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Invoiced"/> - <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Refunded"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Order Rate"/> - <column xsi:type="decimal" name="base_total_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Canceled"/> - <column xsi:type="decimal" name="base_total_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Invoiced"/> - <column xsi:type="decimal" name="base_total_invoiced_cost" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_total_invoiced_cost" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Invoiced Cost"/> - <column xsi:type="decimal" name="base_total_offline_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_total_offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Offline Refunded"/> - <column xsi:type="decimal" name="base_total_online_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_total_online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Online Refunded"/> - <column xsi:type="decimal" name="base_total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Paid"/> <column xsi:type="decimal" name="base_total_qty_ordered" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Total Qty Ordered"/> - <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Refunded"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> - <column xsi:type="decimal" name="discount_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Canceled"/> - <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Invoiced"/> - <column xsi:type="decimal" name="discount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Refunded"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="shipping_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Canceled"/> - <column xsi:type="decimal" name="shipping_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Invoiced"/> - <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Refunded"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="shipping_tax_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Refunded"/> <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" comment="Store To Base Rate"/> <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" comment="Store To Order Rate"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="subtotal_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Canceled"/> - <column xsi:type="decimal" name="subtotal_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Invoiced"/> - <column xsi:type="decimal" name="subtotal_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Refunded"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> - <column xsi:type="decimal" name="tax_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Canceled"/> - <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Invoiced"/> - <column xsi:type="decimal" name="tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Refunded"/> - <column xsi:type="decimal" name="total_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Canceled"/> - <column xsi:type="decimal" name="total_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Invoiced"/> - <column xsi:type="decimal" name="total_offline_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Offline Refunded"/> - <column xsi:type="decimal" name="total_online_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Online Refunded"/> - <column xsi:type="decimal" name="total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Paid"/> <column xsi:type="decimal" name="total_qty_ordered" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Qty Ordered"/> - <column xsi:type="decimal" name="total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Refunded"/> <column xsi:type="smallint" name="can_ship_partially" padding="5" unsigned="true" nullable="true" identity="false" comment="Can Ship Partially"/> @@ -163,27 +163,27 @@ comment="Quote Id"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Shipping Address Id"/> - <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> - <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> - <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Negative"/> - <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Positive"/> - <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Amount"/> - <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Incl Tax"/> - <column xsi:type="decimal" name="base_total_due" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_due" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Due"/> - <column xsi:type="decimal" name="payment_authorization_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="payment_authorization_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Payment Authorization Amount"/> - <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="total_due" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_due" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Due"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> @@ -230,25 +230,25 @@ identity="false" default="0" comment="Total Item Count"/> <column xsi:type="int" name="customer_gender" padding="11" unsigned="false" nullable="true" identity="false" comment="Customer Gender"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Refunded"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Refunded"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> <column xsi:type="varchar" name="coupon_rule_name" nullable="true" length="255" comment="Coupon Sales Rule Name"/> @@ -304,13 +304,13 @@ <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="base_total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Paid"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Paid"/> <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> @@ -326,13 +326,13 @@ comment="Shipping Method Name"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="255" comment="Customer Email"/> <column xsi:type="varchar" name="customer_group" nullable="true" length="255" comment="Customer Group"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping and handling amount"/> <column xsi:type="varchar" name="customer_name" nullable="true" length="255" comment="Customer Name"/> <column xsi:type="varchar" name="payment_method" nullable="true" length="255" comment="Payment Method"/> - <column xsi:type="decimal" name="total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Refunded"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> @@ -513,78 +513,78 @@ comment="Base Original Price"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Tax Percent"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Invoiced"/> - <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Invoiced"/> <column xsi:type="decimal" name="discount_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Discount Percent"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Invoiced"/> - <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Invoiced"/> - <column xsi:type="decimal" name="amount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Amount Refunded"/> - <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Amount Refunded"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Total"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Total"/> - <column xsi:type="decimal" name="row_invoiced" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_invoiced" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Invoiced"/> - <column xsi:type="decimal" name="base_row_invoiced" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_invoiced" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Invoiced"/> <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> - <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Before Discount"/> - <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> <column xsi:type="varchar" name="ext_order_item_id" nullable="true" length="255" comment="Ext Order Item Id"/> <column xsi:type="smallint" name="locked_do_invoice" padding="5" unsigned="true" nullable="true" identity="false" comment="Locked Do Invoice"/> <column xsi:type="smallint" name="locked_do_ship" padding="5" unsigned="true" nullable="true" identity="false" comment="Locked Do Ship"/> - <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> - <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price Incl Tax"/> - <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> - <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Refunded"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Refunded"/> <column xsi:type="decimal" name="tax_canceled" scale="4" precision="12" unsigned="false" nullable="true" comment="Tax Canceled"/> - <column xsi:type="decimal" name="discount_tax_compensation_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Canceled"/> - <column xsi:type="decimal" name="tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Refunded"/> - <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Refunded"/> - <column xsi:type="decimal" name="discount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Refunded"/> - <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Refunded"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="item_id"/> @@ -605,41 +605,41 @@ comment="Entity Id"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent Id"/> - <column xsi:type="decimal" name="base_shipping_captured" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Captured"/> - <column xsi:type="decimal" name="shipping_captured" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Captured"/> - <column xsi:type="decimal" name="amount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Refunded"/> - <column xsi:type="decimal" name="base_amount_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_amount_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Paid"/> - <column xsi:type="decimal" name="amount_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Canceled"/> - <column xsi:type="decimal" name="base_amount_authorized" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_authorized" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Authorized"/> - <column xsi:type="decimal" name="base_amount_paid_online" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_paid_online" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Paid Online"/> - <column xsi:type="decimal" name="base_amount_refunded_online" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_refunded_online" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Refunded Online"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="amount_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Paid"/> - <column xsi:type="decimal" name="amount_authorized" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_authorized" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Authorized"/> - <column xsi:type="decimal" name="base_amount_ordered" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_amount_ordered" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Ordered"/> - <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Refunded"/> - <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Refunded"/> - <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Refunded"/> - <column xsi:type="decimal" name="amount_ordered" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_ordered" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Ordered"/> - <column xsi:type="decimal" name="base_amount_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Canceled"/> <column xsi:type="int" name="quote_payment_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Quote Payment Id"/> @@ -840,9 +840,9 @@ comment="Entity Id"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent Id"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total"/> - <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="true" comment="Price"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> @@ -929,43 +929,43 @@ comment="Entity Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Store Id"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Order Rate"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Order Rate"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Incl Tax"/> - <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Base Rate"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Qty"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Billing Address Id"/> @@ -994,19 +994,19 @@ comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Refunded"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> @@ -1077,15 +1077,15 @@ <column xsi:type="varchar" name="shipping_address" nullable="true" length="255" comment="Shipping Address"/> <column xsi:type="varchar" name="shipping_information" nullable="true" length="255" comment="Shipping Method Name"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping and handling amount"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated At"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> @@ -1143,11 +1143,11 @@ comment="Base Price"/> <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" comment="Discount Amount"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total"/> <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Discount Amount"/> @@ -1220,53 +1220,53 @@ comment="Entity Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Store Id"/> - <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Order Rate"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Order Rate"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Negative"/> - <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Incl Tax"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Base Rate"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="base_adjustment" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_adjustment" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="adjustment" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Positive"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Order Id"/> @@ -1295,17 +1295,17 @@ comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> @@ -1362,7 +1362,7 @@ <column xsi:type="varchar" name="billing_name" nullable="true" length="255" comment="Billing Name"/> <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="Status"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <column xsi:type="varchar" name="order_status" nullable="true" length="32" comment="Order Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" @@ -1376,15 +1376,15 @@ <column xsi:type="varchar" name="payment_method" nullable="true" length="32" comment="Payment Method"/> <column xsi:type="varchar" name="shipping_information" nullable="true" length="255" comment="Shipping Method Name"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping and handling amount"/> - <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> - <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> - <column xsi:type="decimal" name="order_base_grand_total" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="order_base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Order Grand Total"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> @@ -1593,31 +1593,31 @@ default="0" comment="Total Qty Ordered"/> <column xsi:type="decimal" name="total_qty_invoiced" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Total Qty Invoiced"/> - <column xsi:type="decimal" name="total_income_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_income_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Income Amount"/> - <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Revenue Amount"/> - <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Profit Amount"/> - <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Invoiced Amount"/> - <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Canceled Amount"/> - <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Paid Amount"/> - <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Refunded Amount"/> - <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount"/> - <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount Actual"/> - <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount"/> - <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount Actual"/> - <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount"/> - <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount Actual"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> @@ -1647,31 +1647,31 @@ default="0" comment="Total Qty Ordered"/> <column xsi:type="decimal" name="total_qty_invoiced" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Total Qty Invoiced"/> - <column xsi:type="decimal" name="total_income_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_income_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Income Amount"/> - <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Revenue Amount"/> - <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Profit Amount"/> - <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Invoiced Amount"/> - <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Canceled Amount"/> - <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Paid Amount"/> - <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Refunded Amount"/> - <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount"/> - <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount Actual"/> - <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount"/> - <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount Actual"/> - <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount"/> - <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount Actual"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> @@ -1737,11 +1737,11 @@ <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Refunded"/> - <column xsi:type="decimal" name="online_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Online Refunded"/> - <column xsi:type="decimal" name="offline_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Offline Refunded"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> @@ -1767,11 +1767,11 @@ <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Refunded"/> - <column xsi:type="decimal" name="online_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Online Refunded"/> - <column xsi:type="decimal" name="offline_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Offline Refunded"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> @@ -1798,9 +1798,9 @@ comment="Shipping Description"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="total_shipping" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_shipping" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping"/> - <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping Actual"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> @@ -1829,9 +1829,9 @@ comment="Shipping Description"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="total_shipping" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_shipping" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping"/> - <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping Actual"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> @@ -1957,17 +1957,17 @@ <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="decimal" name="percent" scale="4" precision="12" unsigned="false" nullable="true" comment="Percent"/> - <column xsi:type="decimal" name="amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount"/> <column xsi:type="int" name="priority" padding="11" unsigned="false" nullable="false" identity="false" comment="Priority"/> <column xsi:type="int" name="position" padding="11" unsigned="false" nullable="false" identity="false" comment="Position"/> - <column xsi:type="decimal" name="base_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount"/> <column xsi:type="smallint" name="process" padding="6" unsigned="false" nullable="false" identity="false" comment="Process"/> - <column xsi:type="decimal" name="base_real_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_real_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Real Amount"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_id"/> @@ -1987,13 +1987,13 @@ comment="Item Id"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="false" comment="Real Tax Percent For Item"/> - <column xsi:type="decimal" name="amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Tax amount for the item and tax rate"/> - <column xsi:type="decimal" name="base_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Base tax amount for the item and tax rate"/> - <column xsi:type="decimal" name="real_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="real_amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Real tax amount for the item and tax rate"/> - <column xsi:type="decimal" name="real_base_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="real_base_amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Real base tax amount for the item and tax rate"/> <column xsi:type="int" name="associated_item_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Id of the associated item"/> diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 5a5dd925a3098..68fcd17122bd2 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -1015,4 +1015,9 @@ <preference for="Magento\Sales\Api\OrderCustomerDelegateInterface" type="Magento\Sales\Model\Order\OrderCustomerDelegate" /> + <type name="Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker"> + <arguments> + <argument name="productAvailabilityChecks" xsi:type="array" /> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/etc/extension_attributes.xml b/app/code/Magento/Sales/etc/extension_attributes.xml index 7280a1a071548..222f61cdc7324 100644 --- a/app/code/Magento/Sales/etc/extension_attributes.xml +++ b/app/code/Magento/Sales/etc/extension_attributes.xml @@ -10,4 +10,7 @@ <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> <attribute code="shipping_assignments" type="Magento\Sales\Api\Data\ShippingAssignmentInterface[]" /> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="payment_additional_info" type="Magento\Payment\Api\Data\PaymentAdditionalInfoInterface[]" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index cee245e348393..492dff8057039 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -10,271 +10,271 @@ <route url="/V1/orders/:id" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/statuses" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getStatus"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/cancel" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::cancel" /> </resources> </route> <route url="/V1/orders/:id/emails" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::emails" /> </resources> </route> <route url="/V1/orders/:id/hold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="hold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::hold" /> </resources> </route> <route url="/V1/orders/:id/unhold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="unHold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::unhold" /> </resources> </route> <route url="/V1/orders/:id/comments" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="addComment"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::comment" /> </resources> </route> <route url="/V1/orders/:id/comments" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/create" method="PUT"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/:parent_id" method="PUT"> <service class="Magento\Sales\Api\OrderAddressRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/items/:id" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/items" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/invoices/:id" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/comments" method="GET"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/emails" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/void" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setVoid"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/capture" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setCapture"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/comments" method="POST"> <service class="Magento\Sales\Api\InvoiceCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/" method="POST"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoice/:invoiceId/refund" method="POST"> <service class="Magento\Sales\Api\RefundInvoiceInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="GET"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemos" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="PUT"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/emails" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/refund" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="refund"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="POST"> <service class="Magento\Sales\Api\CreditmemoCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo" method="POST"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/order/:orderId/refund" method="POST"> <service class="Magento\Sales\Api\RefundOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::creditmemo" /> </resources> </route> <route url="/V1/shipment/:id" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipments" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="POST"> <service class="Magento\Sales\Api\ShipmentCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/emails" method="POST"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track" method="POST"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track/:id" method="DELETE"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="deleteById"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/" method="POST"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/label" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getLabel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/order/:orderId/ship" method="POST"> <service class="Magento\Sales\Api\ShipOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::ship" /> </resources> </route> <route url="/V1/orders/" method="POST"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/transactions/:id" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/transactions" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/order/:orderId/invoice" method="POST"> <service class="Magento\Sales\Api\InvoiceOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::invoice" /> </resources> </route> </routes> diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 6435445e0ef93..f2cbd14eb8042 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -18,7 +18,7 @@ <type name="Magento\Framework\Reflection\DataObjectProcessor"> <arguments> <argument name="processors" xsi:type="array"> - <item name="\Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + <item name="Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 6435445e0ef93..f2cbd14eb8042 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -18,7 +18,7 @@ <type name="Magento\Framework\Reflection\DataObjectProcessor"> <arguments> <argument name="processors" xsi:type="array"> - <item name="\Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + <item name="Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml index c69d453fb81d5..00fa55d38f5fc 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml @@ -7,7 +7,7 @@ ?> <?php if ($block->hasMethods()) : ?> <div id="order-billing_method_form"> - <dl class="admin__payment-methods"> + <dl class="admin__payment-methods control"> <?php $_methods = $block->getMethods(); $_methodsCount = count($_methods); @@ -28,8 +28,8 @@ <?php if ($currentSelectedMethod == $_code) : ?> checked="checked" <?php endif; ?> - <?php $className = ($_counter == $_methodsCount) ? ' validate-one-required-by-name' : ''; ?> - class="admin__control-radio<?= $block->escapeHtml($className); ?>"/> + data-validate="{'validate-one-required-by-name':true}" + class="admin__control-radio"/> <?php else :?> <span class="no-display"> <input id="p_method_<?= $block->escapeHtml($_code); ?>" 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 b0a88b8fa37dc..d1a90783c68c7 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 @@ -89,11 +89,8 @@ endif; ?> <?= $block->getForm()->toHtml() ?> <div class="admin__field admin__field-option order-save-in-address-book"> - <input name="<?= $block->getForm()->getHtmlNamePrefix() ?>[save_in_address_book]" type="checkbox" - id="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" - value="1" - <?php if (!$block->getDontSaveInAddressBook() && $block->getAddress()->getSaveInAddressBook()): ?> checked="checked"<?php endif; ?> - class="admin__control-checkbox"/> + <input name="<?= $block->getForm()->getHtmlNamePrefix() ?>[save_in_address_book]" type="checkbox" id="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" value="1" + <?php if (!$block->getDontSaveInAddressBook()): ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> <label for="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" class="admin__field-label"><?= /* @escapeNotVerified */ __('Save in address book') ?></label> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml index 92139896273da..643146f7bb5cb 100755 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml @@ -33,7 +33,6 @@ $taxAmount = $block->getTotal()->getValue(); <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> - <?php $isFirst = 1; ?> <?php foreach ($rates as $rate): ?> <tr class="summary-details-<?= /* @escapeNotVerified */ $taxIter ?> summary-details<?php if ($isTop): echo ' summary-details-first'; endif; ?>" style="display:none;"> @@ -44,13 +43,10 @@ $taxAmount = $block->getTotal()->getValue(); <?php endif; ?> <br /> </td> - <?php if ($isFirst): ?> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount" rowspan="<?= count($rates) ?>"> - <?= /* @escapeNotVerified */ $block->formatPrice($amount) ?> - </td> - <?php endif; ?> + <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount"> + <?= /* @escapeNotVerified */ $block->formatPrice(($amount*(float)$rate['percent'])/$percent) ?> + </td> </tr> - <?php $isFirst = 0; ?> <?php $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index fcf4ccad7060b..ba4af32ff69b2 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -136,67 +136,76 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); -var fields = $$('.qty-input'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button'); +var fields = jQuery('.qty-input'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +} -for(var i=0;i<fields.length;i++){ - fields[i].observe('change', checkButtonsRelation) - fields[i].baseValue = fields[i].value; +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); } +disableButtons(updateButtons); + +fields.on('change', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); + function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + enableButtons(submitButtons); + disableButtons(updateButtons); } } submitCreditMemo = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=0; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 0); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); -} +}; submitCreditMemoOffline = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=1; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 1); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); -} - -var sendEmailCheckbox = $('send_email'); +}; -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var creditmemoCommentText = $('creditmemo_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } -function bindSendEmail() -{ - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //creditmemoCommentText.disabled = false; +function bindSendEmail() { + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //creditmemoCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml index 4a77c3b166de9..872be35cff206 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -134,56 +134,61 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button'); var enableSubmitButtons = <?= (int) !$block->getDisableSubmitButton() ?>; -var fields = $$('.qty-input'); +var fields = jQuery('.qty-input'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +} -for(var i=0;i<fields.length;i++){ - jQuery(fields[i]).on('keyup', checkButtonsRelation); - fields[i].baseValue = fields[i].value; +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); } +disableButtons(updateButtons); + +fields.on('keyup', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); + function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { if (enableSubmitButtons) { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + enableButtons(submitButtons); } - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + disableButtons(updateButtons); } } -var sendEmailCheckbox = $('send_email'); -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var invoiceCommentText = $('invoice_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } function bindSendEmail() { - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //invoiceCommentText.disabled = false; + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //invoiceCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index 5384a00dc894d..bbd6394097f9e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -104,7 +104,7 @@ $customerUrl = $block->getCustomerViewUrl(); <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()): ?> <tr> <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getOrderCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> - <th><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></th> + <td><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></td> </tr> <?php endif; ?> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml index f36a7d2821f7a..e0b7dae8fdb1a 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml @@ -92,7 +92,7 @@ <column name="order_increment_id"> <settings> <filter>text</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index fe67f4d5e2de2..e1f047b372c95 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -17,6 +17,7 @@ <url path="sales/order_create/start"/> <class>primary</class> <label translate="true">Create New Order</label> + <aclResource>Magento_Sales::create</aclResource> </button> </buttons> <spinner>sales_order_columns</spinner> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml index e0495e62d5ce1..9e02c31a20635 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml @@ -100,7 +100,7 @@ <column name="order_increment_id"> <settings> <filter>text</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml index 10b7b1c028c66..cf536c27a0ac3 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml @@ -105,7 +105,7 @@ <column name="order_increment_id"> <settings> <filter>text</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml index 6db77a79b8c14..5f8ebde290664 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml @@ -106,7 +106,7 @@ <column name="order_increment_id"> <settings> <filter>textRange</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html index 3a4aab19e9e7c..a6a10fb49e3f5 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html @@ -11,7 +11,7 @@ "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var order.getCustomerName()":"Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html index bc7c079d7f21b..b7411d80d2ba6 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html @@ -10,7 +10,7 @@ "var creditmemo.increment_id":"Credit Memo Id", "var billing.getName()":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update.html b/app/code/Magento/Sales/view/frontend/email/invoice_update.html index cafdd65ff5208..4043e59f9d7d6 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update.html @@ -11,7 +11,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html index fafb533301efb..40cdec7fb4cab 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html @@ -10,7 +10,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_update.html b/app/code/Magento/Sales/view/frontend/email/order_update.html index a709a9ed8a7f1..a8f0068b70e87 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html index 5a39b01810c18..749fa3b60ad59 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -22,7 +22,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new.html b/app/code/Magento/Sales/view/frontend/email/shipment_new.html index 8af49f322c682..84f5acb29ea3b 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new.html @@ -53,7 +53,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html index df1677f56a500..bb181126724da 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -51,7 +51,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update.html b/app/code/Magento/Sales/view/frontend/email/shipment_update.html index 6d9efc37004bc..9d1c93287549a 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html index 4896a00b7bc5a..0d2dccd3377d2 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml new file mode 100644 index 0000000000000..91414663951d3 --- /dev/null +++ b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="sales_email_order_shipment_renderers"/> + <body> + <block class="Magento\Framework\View\Element\Template" name="sales.order.email.shipment.track" template="Magento_Sales::email/shipment/track.phtml"/> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml index 1fca65932b0b0..20c2c1869fedb 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml @@ -31,6 +31,6 @@ </td> <td class="item-qty"><?= /* @escapeNotVerified */ $_item->getQty() * 1 ?></td> <td class="item-price"> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item->getOrderItem()) ?> + <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml index 1c02a5c31ea6b..b9a032212352b 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml @@ -6,6 +6,8 @@ // @codingStandardsIgnoreFile +/** @var \Magento\Sales\Block\Order\History $block */ + ?> <?php $_orders = $block->getOrders(); ?> <?= $block->getChildHtml('info') ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml index 9b3633fde60b4..a2ab3d02b13ea 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -57,7 +57,7 @@ </button> </div> <div class="secondary"> - <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>"> + <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>#my-orders-table"> <span><?= /* @escapeNotVerified */ __('View All') ?></span> </a> </div> diff --git a/app/code/Magento/SalesAnalytics/composer.json b/app/code/Magento/SalesAnalytics/composer.json index 64424c8f5bc61..b77dcd7e71c65 100644 --- a/app/code/Magento/SalesAnalytics/composer.json +++ b/app/code/Magento/SalesAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-sales": "*" + "magento/module-sales": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php index 5802115d44b5e..3e592cf061dfc 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -11,7 +11,7 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Sales\Model\ResourceModel\Order\CollectionFactoryInterface; -use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; /** * Orders data reslover @@ -24,20 +24,20 @@ class Orders implements ResolverInterface private $collectionFactory; /** - * @var CheckCustomerAccount + * @var GetCustomer */ - private $checkCustomerAccount; + private $getCustomer; /** * @param CollectionFactoryInterface $collectionFactory - * @param CheckCustomerAccount $checkCustomerAccount + * @param GetCustomer $getCustomer */ public function __construct( CollectionFactoryInterface $collectionFactory, - CheckCustomerAccount $checkCustomerAccount + GetCustomer $getCustomer ) { $this->collectionFactory = $collectionFactory; - $this->checkCustomerAccount = $checkCustomerAccount; + $this->getCustomer = $getCustomer; } /** @@ -50,11 +50,10 @@ public function resolve( array $value = null, array $args = null ) { - $customerId = $context->getUserId(); - $this->checkCustomerAccount->execute($customerId, $context->getUserType()); + $customer = $this->getCustomer->execute($context); $items = []; - $orders = $this->collectionFactory->create($customerId); + $orders = $this->collectionFactory->create($customer->getId()); /** @var \Magento\Sales\Model\Order $order */ foreach ($orders as $order) { diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php index da05fd98e609b..cfafe110df22b 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php @@ -7,9 +7,13 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\SalesRule\Model\CouponGenerator; +use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory; /** * Generate promo quote + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpPostActionInterface { @@ -18,6 +22,16 @@ class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote imple */ private $couponGenerator; + /** + * @var PublisherInterface + */ + private $messagePublisher; + + /** + * @var CouponGenerationSpecInterfaceFactory + */ + private $generationSpecFactory; + /** * Generate constructor. * @param \Magento\Backend\App\Action\Context $context @@ -25,17 +39,27 @@ class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote imple * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter * @param CouponGenerator|null $couponGenerator + * @param PublisherInterface|null $publisher + * @param CouponGenerationSpecInterfaceFactory|null $generationSpecFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - CouponGenerator $couponGenerator = null + CouponGenerator $couponGenerator = null, + PublisherInterface $publisher = null, + CouponGenerationSpecInterfaceFactory $generationSpecFactory = null ) { parent::__construct($context, $coreRegistry, $fileFactory, $dateFilter); $this->couponGenerator = $couponGenerator ?: $this->_objectManager->get(CouponGenerator::class); + $this->messagePublisher = $publisher ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(PublisherInterface::class); + $this->generationSpecFactory = $generationSpecFactory ?: + \Magento\Framework\App\ObjectManager::getInstance()->get( + CouponGenerationSpecInterfaceFactory::class + ); } /** @@ -64,9 +88,14 @@ public function execute() $data = $inputFilter->getUnescaped(); } - $couponCodes = $this->couponGenerator->generateCodes($data); - $generated = count($couponCodes); - $this->messageManager->addSuccessMessage(__('%1 coupon(s) have been generated.', $generated)); + $data['quantity'] = isset($data['qty']) ? $data['qty'] : null; + + $couponSpec = $this->generationSpecFactory->create(['data' => $data]); + + $this->messagePublisher->publish('sales_rule.codegenerator', $couponSpec); + $this->messageManager->addSuccessMessage( + __('Message is added to queue, wait to get your coupons soon') + ); $this->_view->getLayout()->initMessages(); $result['messages'] = $this->_view->getLayout()->getMessagesBlock()->getGroupedHtml(); } catch (\Magento\Framework\Exception\InputException $inputException) { diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php index a0238890d98af..50545fd864866 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class NewConditionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Controller class NewConditionHtml. Returns condition html + */ +class NewConditionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpPostActionInterface { /** * New condition html action diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php index 388679e6d9eff..7d55d18b770e2 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php @@ -7,6 +7,7 @@ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** @@ -20,6 +21,11 @@ class Save extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implement * @var TimezoneInterface */ private $timezone; + + /** + * @var DataPersistorInterface + */ + private $dataPersistor; /** * @param \Magento\Backend\App\Action\Context $context @@ -27,18 +33,23 @@ class Save extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implement * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter * @param TimezoneInterface $timezone + * @param DataPersistorInterface $dataPersistor */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - TimezoneInterface $timezone = null + TimezoneInterface $timezone = null, + DataPersistorInterface $dataPersistor = null ) { parent::__construct($context, $coreRegistry, $fileFactory, $dateFilter); $this->timezone = $timezone ?? \Magento\Framework\App\ObjectManager::getInstance()->get( TimezoneInterface::class ); + $this->dataPersistor = $dataPersistor ?? \Magento\Framework\App\ObjectManager::getInstance()->get( + DataPersistorInterface::class + ); } /** @@ -73,12 +84,8 @@ public function execute() $data ); $data = $inputFilter->getUnescaped(); - $id = $this->getRequest()->getParam('rule_id'); - if ($id) { - $model->load($id); - if ($id != $model->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The wrong rule is specified.')); - } + if (!$this->checkRuleExists($model)) { + throw new \Magento\Framework\Exception\LocalizedException(__('The wrong rule is specified.')); } $session = $this->_objectManager->get(\Magento\Backend\Model\Session::class); @@ -89,6 +96,7 @@ public function execute() $this->messageManager->addErrorMessage($errorMessage); } $session->setPageData($data); + $this->dataPersistor->set('sale_rule', $data); $this->_redirect('sales_rule/*/edit', ['id' => $model->getId()]); return; } @@ -147,4 +155,22 @@ public function execute() } $this->_redirect('sales_rule/*/'); } + + /** + * Check if Cart Price Rule with provided id exists. + * + * @param \Magento\SalesRule\Model\Rule $model + * @return bool + */ + private function checkRuleExists(\Magento\SalesRule\Model\Rule $model): bool + { + $id = $this->getRequest()->getParam('rule_id'); + if ($id) { + $model->load($id); + if ($model->getId() != $id) { + return false; + } + } + return true; + } } diff --git a/app/code/Magento/SalesRule/Model/Coupon/Consumer.php b/app/code/Magento/SalesRule/Model/Coupon/Consumer.php new file mode 100644 index 0000000000000..2354c72a3e293 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Consumer.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\SalesRule\Api\CouponManagementInterface; +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterface; +use Magento\Framework\Notification\NotifierInterface; + +/** + * Consumer for export coupons generation. + */ +class Consumer +{ + /** + * @var NotifierInterface + */ + private $notifier; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var CouponManagementInterface + */ + private $couponManager; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * Consumer constructor. + * @param \Psr\Log\LoggerInterface $logger + * @param CouponManagementInterface $couponManager + * @param Filesystem $filesystem + * @param NotifierInterface $notifier + */ + public function __construct( + \Psr\Log\LoggerInterface $logger, + CouponManagementInterface $couponManager, + Filesystem $filesystem, + NotifierInterface $notifier + ) { + $this->logger = $logger; + $this->couponManager = $couponManager; + $this->filesystem = $filesystem; + $this->notifier = $notifier; + } + + /** + * Consumer logic. + * + * @param CouponGenerationSpecInterface $exportInfo + * @return void + */ + public function process(CouponGenerationSpecInterface $exportInfo) + { + try { + $this->couponManager->generate($exportInfo); + + $this->notifier->addMajor( + __('Your coupons are ready'), + __('You can check your coupons at sales rule page') + ); + } catch (LocalizedException $exception) { + $this->notifier->addCritical( + __('Error during coupons generator process occurred'), + __('Error during coupons generator process occurred. Please check logs for detail') + ); + $this->logger->critical( + 'Something went wrong while coupons generator process. ' . $exception->getMessage() + ); + } + } +} diff --git a/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php b/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php new file mode 100644 index 0000000000000..c37ca276e0ee2 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Quote\Address\Total; + +use Magento\Quote\Api\Data\ShippingAssignmentInterface as ShippingAssignment; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\SalesRule\Model\Quote\Discount as DiscountCollector; +use Magento\SalesRule\Model\Validator; + +/** + * Total collector for shipping discounts. + */ +class ShippingDiscount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal +{ + /** + * @var Validator + */ + private $calculator; + + /** + * @param Validator $calculator + */ + public function __construct(Validator $calculator) + { + $this->calculator = $calculator; + } + + /** + * @inheritdoc + * + * @param Quote $quote + * @param ShippingAssignment $shippingAssignment + * @param Total $total + * @return ShippingDiscount + */ + public function collect(Quote $quote, ShippingAssignment $shippingAssignment, Total $total): self + { + parent::collect($quote, $shippingAssignment, $total); + + $address = $shippingAssignment->getShipping()->getAddress(); + $this->calculator->reset($address); + + $items = $shippingAssignment->getItems(); + if (!count($items)) { + return $this; + } + + $address->setShippingDiscountAmount(0); + $address->setBaseShippingDiscountAmount(0); + if ($address->getShippingAmount()) { + $this->calculator->processShippingAmount($address); + $total->addTotalAmount(DiscountCollector::COLLECTOR_TYPE_CODE, -$address->getShippingDiscountAmount()); + $total->addBaseTotalAmount( + DiscountCollector::COLLECTOR_TYPE_CODE, + -$address->getBaseShippingDiscountAmount() + ); + $total->setShippingDiscountAmount($address->getShippingDiscountAmount()); + $total->setBaseShippingDiscountAmount($address->getBaseShippingDiscountAmount()); + + $this->calculator->prepareDescription($address); + $total->setDiscountDescription($address->getDiscountDescription()); + $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); + $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + + $address->setDiscountAmount($total->getDiscountAmount()); + $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + } + + return $this; + } + + /** + * @inheritdoc + * + * @param \Magento\Quote\Model\Quote $quote + * @param \Magento\Quote\Model\Quote\Address\Total $total + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function fetch(Quote $quote, Total $total): array + { + $result = []; + $amount = $total->getDiscountAmount(); + + if ($amount != 0) { + $description = $total->getDiscountDescription() ?: ''; + $result = [ + 'code' => DiscountCollector::COLLECTOR_TYPE_CODE, + 'title' => strlen($description) ? __('Discount (%1)', $description) : __('Discount'), + 'value' => $amount + ]; + } + return $result; + } +} diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 693a61b272f66..315ce874513a3 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -5,8 +5,13 @@ */ namespace Magento\SalesRule\Model\Quote; +/** + * Discount totals calculation model. + */ class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { + const COLLECTOR_TYPE_CODE = 'discount'; + /** * Discount calculation object * @@ -43,7 +48,7 @@ public function __construct( \Magento\SalesRule\Model\Validator $validator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency ) { - $this->setCode('discount'); + $this->setCode(self::COLLECTOR_TYPE_CODE); $this->eventManager = $eventManager; $this->calculator = $validator; $this->storeManager = $storeManager; @@ -124,21 +129,14 @@ public function collect( } } - /** Process shipping amount discount */ - $address->setShippingDiscountAmount(0); - $address->setBaseShippingDiscountAmount(0); - if ($address->getShippingAmount()) { - $this->calculator->processShippingAmount($address); - $total->addTotalAmount($this->getCode(), -$address->getShippingDiscountAmount()); - $total->addBaseTotalAmount($this->getCode(), -$address->getBaseShippingDiscountAmount()); - $total->setShippingDiscountAmount($address->getShippingDiscountAmount()); - $total->setBaseShippingDiscountAmount($address->getBaseShippingDiscountAmount()); - } - $this->calculator->prepareDescription($address); $total->setDiscountDescription($address->getDiscountDescription()); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + + $address->setDiscountAmount($total->getDiscountAmount()); + $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + return $this; } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 59f24fa8b6e03..3e7b6ea281501 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -9,6 +9,9 @@ use Magento\Framework\DB\Select; use Magento\Framework\Serialize\Serializer\Json; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Api\Data\CouponInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; /** * Sales Rules resource collection model. @@ -80,6 +83,8 @@ protected function _construct() } /** + * Map data for associated entities + * * @param string $entityType * @param string $objectField * @throws \Magento\Framework\Exception\LocalizedException @@ -105,15 +110,20 @@ protected function mapAssociatedEntities($entityType, $objectField) $associatedEntities = $this->getConnection()->fetchAll($select); - array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, $associatedEntities); + array_map( + function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { + $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); + $itemAssociatedValue = $item->getData($objectField) ?? []; + $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; + $item->setData($objectField, $itemAssociatedValue); + }, + $associatedEntities + ); } /** + * Add website ids and customer group ids to rules data + * * @return $this * @throws \Exception * @since 100.1.0 @@ -137,6 +147,7 @@ protected function _afterLoad() * @param string $couponCode * @param string|null $now * @param Address $address allow extensions to further filter out rules based on quote address + * @throws \Zend_Db_Select_Exception * @use $this->addWebsiteGroupDateFilter() * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return $this @@ -149,77 +160,24 @@ public function setValidationFilter( Address $address = null ) { if (!$this->getFlag('validation_filter')) { - /* We need to overwrite joinLeft if coupon is applied */ - $this->getSelect()->reset(); - parent::_initSelect(); + $this->prepareSelect($websiteId, $customerGroupId, $now); - $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); - $select = $this->getSelect(); + $noCouponRules = $this->getNoCouponCodeSelect(); - $connection = $this->getConnection(); - if (strlen($couponCode)) { - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ), - ['code'] - ); - - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ? ', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); - - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); - - $select->where( - $noCouponWhereCondition . ' OR ((' . $orWhereCondition . ') AND ' . $andWhereCondition . ')', - null, - Select::TYPE_CONDITION - ); + if ($couponCode) { + $couponRules = $this->getCouponCodeSelect($couponCode); + + $allAllowedRules = $this->getConnection()->select(); + $allAllowedRules->union([$noCouponRules, $couponRules], Select::SQL_UNION_ALL); + + $wrapper = $this->getConnection()->select(); + $wrapper->from($allAllowedRules); + + $this->_select = $wrapper; } else { - $this->addFieldToFilter( - 'main_table.coupon_type', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); + $this->_select = $noCouponRules; } + $this->setOrder('sort_order', self::SORT_ORDER_ASC); $this->setFlag('validation_filter', true); } @@ -227,6 +185,101 @@ public function setValidationFilter( return $this; } + /** + * Recreate the default select object for specific needs of salesrule evaluation with coupon codes. + * + * @param int $websiteId + * @param int $customerGroupId + * @param string $now + */ + private function prepareSelect($websiteId, $customerGroupId, $now) + { + $this->getSelect()->reset(); + parent::_initSelect(); + + $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); + } + + /** + * Return select object to determine all active rules not needing a coupon code. + * + * @return Select + */ + private function getNoCouponCodeSelect() + { + $noCouponSelect = clone $this->getSelect(); + + $noCouponSelect->where( + 'main_table.coupon_type = ?', + Rule::COUPON_TYPE_NO_COUPON + ); + + $noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]); + + return $noCouponSelect; + } + + /** + * Determine all active rules that are valid for the given coupon code. + * + * @param string $couponCode + * @return Select + */ + private function getCouponCodeSelect($couponCode) + { + $couponSelect = clone $this->getSelect(); + + $this->joinCouponTable($couponCode, $couponSelect); + + $notExpired = $this->getConnection()->quoteInto( + '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', + $this->_date->date()->format('Y-m-d') + ); + + $isAutogenerated = + $this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED); + + $isValidSpecific = + $this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC) + . ' AND (' . + '(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)' + . ' OR ' . + '(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)' + . ')'; + + $couponSelect->where( + "$notExpired AND ($isAutogenerated OR $isValidSpecific)", + null, + Select::TYPE_CONDITION + ); + + return $couponSelect; + } + + /** + * Join coupong table to select. + * + * @param string $couponCode + * @param Select $couponSelect + */ + private function joinCouponTable($couponCode, Select $couponSelect) + { + $couponJoinCondition = + 'main_table.rule_id = rule_coupons.rule_id' + . ' AND ' . + $this->getConnection()->quoteInto('main_table.coupon_type <> ?', Rule::COUPON_TYPE_NO_COUPON) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.code = ?', $couponCode); + + $couponSelect->joinInner( + ['rule_coupons' => $this->getTable('salesrule_coupon')], + $couponJoinCondition, + [Coupon::KEY_CODE] + ); + } + /** * Filter collection by website(s), customer group(s) and date. * Filter collection to only active rules. @@ -366,6 +419,8 @@ public function addCustomerGroupFilter($customerGroupId) } /** + * Getter for _associatedEntitiesMap property + * * @return array * @deprecated 100.1.0 */ @@ -380,6 +435,8 @@ private function getAssociatedEntitiesMap() } /** + * Getter for dateApplier property + * * @return DateApplier * @deprecated 100.1.0 */ diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index 89ec2b84572fc..cf6301cb31a9c 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -61,6 +61,7 @@ public function __construct( public function loadAttributeOptions() { $attributes = [ + 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php index 9bda4793e8681..ff83bb1ee9129 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php @@ -35,12 +35,13 @@ protected function _addSpecialAttributes(array &$attributes) * * @return string */ - public function getAttribute() + public function getAttribute(): string { $attribute = $this->getData('attribute'); if (strpos($attribute, '::') !== false) { - list (, $attribute) = explode('::', $attribute); + list(, $attribute) = explode('::', $attribute); } + return $attribute; } @@ -53,6 +54,7 @@ public function getAttributeName() if ($this->getAttributeScope()) { $attribute = $this->getAttributeScope() . '::' . $attribute; } + return $this->getAttributeOption($attribute); } @@ -92,6 +94,7 @@ public function getAttributeElementHtml() { $html = parent::getAttributeElementHtml() . $this->getAttributeScopeElement()->getHtml(); + return $html; } @@ -100,7 +103,7 @@ public function getAttributeElementHtml() * * @return \Magento\Framework\Data\Form\Element\AbstractElement */ - private function getAttributeScopeElement() + private function getAttributeScopeElement(): \Magento\Framework\Data\Form\Element\AbstractElement { return $this->getForm()->addField( $this->getPrefix() . '__' . $this->getId() . '__attribute_scope', @@ -110,7 +113,7 @@ private function getAttributeScopeElement() 'value' => $this->getAttributeScope(), 'no_span' => true, 'class' => 'hidden', - 'data-form-part' => $this->getFormName() + 'data-form-part' => $this->getFormName(), ] ); } @@ -119,8 +122,9 @@ private function getAttributeScopeElement() * Set attribute value * * @param string $value + * @return void */ - public function setAttribute($value) + public function setAttribute(string $value) { if (strpos($value, '::') !== false) { list($scope, $attribute) = explode('::', $value); @@ -137,7 +141,8 @@ public function setAttribute($value) public function loadArray($arr) { parent::loadArray($arr); - $this->setAttributeScope(isset($arr['attribute_scope']) ? $arr['attribute_scope'] : null); + $this->setAttributeScope($arr['attribute_scope'] ?? null); + return $this; } @@ -148,6 +153,7 @@ public function asArray(array $arrAttributes = []) { $out = parent::asArray($arrAttributes); $out['attribute_scope'] = $this->getAttributeScope(); + return $out; } @@ -155,7 +161,9 @@ public function asArray(array $arrAttributes = []) * Validate Product Rule Condition * * @param \Magento\Framework\Model\AbstractModel $model + * * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function validate(\Magento\Framework\Model\AbstractModel $model) { diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php index 1e8fbf43ec3bc..5b02d3c080938 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php @@ -5,6 +5,9 @@ */ namespace Magento\SalesRule\Model\Rule\Condition\Product; +/** + * Subselect conditions for product. + */ class Subselect extends \Magento\SalesRule\Model\Rule\Condition\Product\Combine { /** @@ -161,7 +164,9 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) } } if ($hasValidChild || parent::validate($item)) { - $total += (($hasValidChild && $useChildrenTotal) ? $childrenAttrTotal : $item->getData($attr)); + $total += ($hasValidChild && $useChildrenTotal) + ? $childrenAttrTotal * $item->getQty() + : $item->getData($attr); } } return $this->validateAttribute($total); diff --git a/app/code/Magento/SalesRule/Model/Rule/DataProvider.php b/app/code/Magento/SalesRule/Model/Rule/DataProvider.php index 916825776373d..25f0ef91eae68 100644 --- a/app/code/Magento/SalesRule/Model/Rule/DataProvider.php +++ b/app/code/Magento/SalesRule/Model/Rule/DataProvider.php @@ -5,6 +5,7 @@ */ namespace Magento\SalesRule\Model\Rule; +use Magento\Framework\App\Request\DataPersistorInterface; use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory; use Magento\SalesRule\Model\Rule; @@ -36,6 +37,11 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider */ protected $metadataValueProvider; + /** + * @var DataPersistorInterface + */ + private $dataPersistor; + /** * Initialize dependencies. * @@ -47,6 +53,7 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param Metadata\ValueProvider $metadataValueProvider * @param array $meta * @param array $data + * @param DataPersistorInterface $dataPersistor */ public function __construct( $name, @@ -56,12 +63,16 @@ public function __construct( \Magento\Framework\Registry $registry, \Magento\SalesRule\Model\Rule\Metadata\ValueProvider $metadataValueProvider, array $meta = [], - array $data = [] + array $data = [], + DataPersistorInterface $dataPersistor = null ) { $this->collection = $collectionFactory->create(); $this->coreRegistry = $registry; $this->metadataValueProvider = $metadataValueProvider; $meta = array_replace_recursive($this->getMetadataValues(), $meta); + $this->dataPersistor = $dataPersistor ?? \Magento\Framework\App\ObjectManager::getInstance()->get( + DataPersistorInterface::class + ); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); } @@ -77,7 +88,7 @@ protected function getMetadataValues() } /** - * {@inheritdoc} + * @inheritdoc */ public function getData() { @@ -93,6 +104,13 @@ public function getData() $this->loadedData[$rule->getId()] = $rule->getData(); } + $data = $this->dataPersistor->get('sale_rule'); + if (!empty($data)) { + $rule = $this->collection->getNewEmptyItem(); + $rule->setData($data); + $this->loadedData[$rule->getId()] = $rule->getData(); + $this->dataPersistor->clear('sale_rule'); + } return $this->loadedData; } diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 5c0f97ae0b08b..ea0221d8f072d 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -182,6 +182,8 @@ protected function _getRules(Address $address = null) } /** + * Address id getter. + * * @param Address $address * @return string */ @@ -327,21 +329,7 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $rule->getDiscountAmount(); break; case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: - $cartRules = $address->getCartFixedRules(); - if (!isset($cartRules[$rule->getId()])) { - $cartRules[$rule->getId()] = $rule->getDiscountAmount(); - } - if ($cartRules[$rule->getId()] > 0) { - $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $quote->getStore()); - $discountAmount = min($shippingAmount - $address->getShippingDiscountAmount(), $quoteAmount); - $baseDiscountAmount = min( - $baseShippingAmount - $address->getBaseShippingDiscountAmount(), - $cartRules[$rule->getId()] - ); - $cartRules[$rule->getId()] -= $baseDiscountAmount; - } - - $address->setCartFixedRules($cartRules); + // Shouldn't be proceed according to MAGETWO-96403 break; } @@ -519,6 +507,8 @@ public function sortItemsByPriority($items, Address $address = null) } /** + * Rule total items getter. + * * @param int $key * @return array * @throws \Magento\Framework\Exception\LocalizedException @@ -533,6 +523,8 @@ public function getRuleItemTotalsInfo($key) } /** + * Decrease rule items count. + * * @param int $key * @return $this */ diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml index e5907e1e9c0f5..210259f474ee9 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml @@ -38,4 +38,27 @@ <click selector="{{AdminMainActionsSection.saveAndContinue}}" stepKey="clickSaveButton"/> <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> </actionGroup> + + <actionGroup name="SetConditionForActionsInCartPriceRuleActionGroup"> + <arguments> + <argument name="actionsAggregator" type="string" defaultValue="ANY"/> + <argument name="actionsValue" type="string" defaultValue="FALSE"/> + <argument name="childAttribute" type="string" defaultValue="Category"/> + <argument name="actionValue" type="string"/> + </arguments> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickOnActionTab"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('ALL')}}" stepKey="clickToChooseOption"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsAggregator}}" userInput="{{actionsAggregator}}" stepKey="selectCondition"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('TRUE')}}" stepKey="clickToChooseOption2"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsValue}}" userInput="{{actionsValue}}" stepKey="selectCondition2"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="selectActionConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="{{childAttribute}}" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption3"/> + <fillField selector="{{AdminCartPriceRulesFormSection.actionValue}}" userInput="{{actionValue}}" stepKey="fillActionValue"/> + <click selector="{{AdminCartPriceRulesFormSection.applyAction}}" stepKey="applyAction"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml index b6a825766395d..cc165e0b5dc96 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version="1.0" encoding="UTF-8"?> <!-- /** * Copyright © Magento, Inc. All rights reserved. @@ -49,6 +49,37 @@ <click selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="confirmDelete"/> </actionGroup> + <actionGroup name="AdminCreateCartPriceRuleWithConditions" extends="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="condition1" type="string" defaultValue="Products subselection" /> + <argument name="condition2" type="string" defaultValue="Category" /> + <argument name="ruleToChange1" type="string" defaultValue="is" /> + <argument name="rule1" type="string" defaultValue="equals or greater than" /> + <argument name="ruleToChange2" type="string" defaultValue="..." /> + <argument name="rule2" type="string" defaultValue="2" /> + <argument name="categoryName" type="string" defaultValue="_defaultCategory.name" /> + </arguments> + <remove keyForRemoval="fillDiscountAmount" /> + <!--Go to Conditions section--> + <click selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" stepKey="openConditionsSection" after="selectActionType" /> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1')}}" stepKey="addFirstCondition" after="openConditionsSection" /> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1')}}" userInput="{{condition1}}" stepKey="selectRule" after="addFirstCondition" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange1)}}" stepKey="waitForFirstRuleElement" after="selectRule" /> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange1)}}" stepKey="clickToChangeRule" after="waitForFirstRuleElement" /> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleParameterSelect('1--1')}}" userInput="{{rule1}}" stepKey="selectRule1" after="clickToChangeRule" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="waitForSecondRuleElement" after="selectRule1" /> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="clickToChangeRule1" after="waitForSecondRuleElement" /> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleParameterInput('1--1')}}" userInput="{{rule2}}" stepKey="fillRule" after="clickToChangeRule1" /> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1--1')}}" stepKey="addSecondCondition" after="fillRule" /> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1--1')}}" userInput="{{condition2}}" stepKey="selectSecondCondition" after="addSecondCondition" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="waitForThirdRuleElement" after="selectSecondCondition" /> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="addThirdCondition" after="waitForThirdRuleElement" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.openChooser('1--1--1')}}" stepKey="waitForForthRuleElement" after="addThirdCondition" /> + <click selector="{{AdminCartPriceRulesFormSection.openChooser('1--1--1')}}" stepKey="openChooser" after="waitForForthRuleElement" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="waitForCategoryVisible" after="openChooser" /> + <checkOption selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="checkCategoryName" after="waitForCategoryVisible" /> + </actionGroup> + <actionGroup name="CreateCartPriceRuleSecondWebsiteActionGroup"> <arguments> <argument name="ruleName"/> @@ -58,5 +89,6 @@ <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ruleName.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Second Website" stepKey="selectWebsites"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenNewCartPriceRuleFormPageActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenNewCartPriceRuleFormPageActionGroup.xml new file mode 100644 index 0000000000000..8cb958c9d9304 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenNewCartPriceRuleFormPageActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AdminOpenNewCartPriceRuleFormPageActionGroup"> + <amOnPage url="{{PriceRuleNewPage.url}}" stepKey="openNewCartPriceRulePage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCartPriceRuleFormActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCartPriceRuleFormActionGroup.xml new file mode 100644 index 0000000000000..98a094aea77ac --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCartPriceRuleFormActionGroup.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="AssertCustomerGroupNotOnCartPriceRuleFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <grabMultiple selector="{{AdminCartPriceRulesFormSection.customerGroupsOptions}}" stepKey="customerGroups" /> + <assertNotContains stepKey="assertCustomerGroupNotInOptions"> + <actualResult type="variable">customerGroups</actualResult> + <expectedResult type="string">{{customerGroup.code}}</expectedResult> + </assertNotContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml index a196bbd61d0ac..3e55eb4f26607 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml @@ -32,8 +32,8 @@ <!-- Check applied discount in cart summary --> <actionGroup name="StorefrontCheckCouponAppliedActionGroup"> <arguments> - <argument name="rule"/> - <argument name="discount" type="string"/> + <argument name="rule" /> + <argument name="discount" type="string" /> </arguments> <waitForElementVisible selector="{{CheckoutCartSummarySection.discountTotal}}" stepKey="waitForDiscountTotal"/> <see userInput="{{rule.store_labels[1][store_label]}}" selector="{{CheckoutCartSummarySection.discountLabel}}" stepKey="assertDiscountLabel"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..5a42980df1bbf --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/AdminMenuData.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="AdminMenuMarketingPromotionsCartPriceRules"> + <data key="pageTitle">Cart Price Rules</data> + <data key="title">Cart Price Rules</data> + <data key="dataUiId">magento-salesrule-promo-quote</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index ded1d2806a684..8f6e63534b0ca 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -89,6 +89,16 @@ <data key="apply">Percent of product price discount</data> <data key="discountAmount">50</data> </entity> + <entity name="CatPriceRule" type="SalesRule"> + <data key="name" unique="suffix">CartPriceRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="coupon_type">Specific Coupon</data> + <data key="coupon_code" unique="suffix">CouponCode</data> + <data key="apply">Percent of product price discount</data> + <data key="discountAmount">10</data> + </entity> + <entity name="SalesRuleSpecificCoupon" type="SalesRule"> <data key="name" unique="suffix">SimpleSalesRule</data> <data key="description">Sales Rule Description</data> @@ -145,6 +155,14 @@ <data key="simple_free_shipping">1</data> </entity> + <entity name="PriceRuleWithCondition" type="SalesRule"> + <data key="name" unique="suffix">SalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Fixed amount discount for whole cart</data> + <data key="discountAmount">50</data> + </entity> + <entity name="SalesRuleSpecificCouponWithPercentDiscount" type="SalesRule"> <data key="name" unique="suffix">SimpleSalesRule</data> <data key="description">Sales Rule Description</data> @@ -171,4 +189,8 @@ <data key="uses_per_coupon">10</data> <data key="simple_free_shipping">1</data> </entity> + + <entity name="SalesRuleNoCouponWithFixedDiscount" extends="ApiCartRule"> + <data key="simple_action">by_fixed</data> + </entity> </entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index c3680e1c3ceaf..8af28a7fcb33e 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -18,6 +18,7 @@ <element name="ruleName" type="input" selector="input[name='name']"/> <element name="websites" type="multiselect" selector="select[name='website_ids']"/> <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> + <element name="customerGroupsOptions" type="multiselect" selector="select[name='customer_group_ids'] option"/> <element name="coupon" type="select" selector="select[name='coupon_type']"/> <element name="couponCode" type="input" selector="input[name='coupon_code']"/> <element name="useAutoGeneration" type="checkbox" selector="input[name='use_auto_generation']"/> @@ -32,6 +33,13 @@ <element name="conditionsHeaderOpen" type="button" selector="div[data-index='conditions'] div[data-state-collapsible='open']" timeout="30"/> <element name="conditionsValue" type="input" selector=".rule-param-edit input"/> <element name="conditionsOperator" type="select" selector=".rule-param-edit select"/> + <element name="addCondition" type="button" selector="//*[@id='conditions__{{arg}}__children']//span" parameterized="true"/> + <element name="ruleCondition" type="select" selector="rule[conditions][{{arg}}][new_child]" parameterized="true"/> + <element name="ruleParameter" type="text" selector="//span[@class='rule-param']/a[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="ruleParameterSelect" type="select" selector="rule[conditions][{{arg}}][operator]" parameterized="true"/> + <element name="ruleParameterInput" type="input" selector="rule[conditions][{{arg}}][value]" parameterized="true"/> + <element name="openChooser" type="button" selector="//label[@for='conditions__{{arg}}__value']" parameterized="true"/> + <element name="categoryCheckbox" type="checkbox" selector="//span[contains(text(), '{{arg}}')]/parent::a/preceding-sibling::input[@type='checkbox']" parameterized="true"/> <!-- Actions sub-form --> <element name="actionsTab" type="text" selector="//div[@data-index='actions']//span[contains(.,'Actions')][1]"/> @@ -41,14 +49,18 @@ <element name="conditions" type="button" selector=".rule-param.rule-param-new-child > a"/> <element name="childAttribute" type="select" selector="//select[contains(@name, 'new_child')]"/> <element name="condition" type="text" selector="//span[@class='rule-param']/a[text()='{{arg}}']" parameterized="true"/> + <element name="actionsAggregator" type="select" selector="#actions__1__aggregator"/> + <element name="actionsValue" type="select" selector="#actions__1__value"/> <element name="operator" type="select" selector="//select[contains(@name, '[operator]')]"/> <element name="option" type="select" selector="//ul[@class='rule-param-children']//select[contains(@name, '[value]')]"/> <element name="actionValue" type="input" selector=".rule-param-edit input"/> + <element name="applyAction" type="text" selector=".rule-param-apply" timeout="30"/> <element name="actionOperator" type="select" selector=".rule-param-edit select"/> <element name="applyDiscountToShipping" type="checkbox" selector="input[name='apply_to_shipping']"/> <element name="applyDiscountToShippingLabel" type="checkbox" selector="input[name='apply_to_shipping']+label"/> <element name="discountAmount" type="input" selector="input[name='discount_amount']"/> <element name="discountStep" type="input" selector="input[name='discount_step']"/> + <element name="addRewardPoints" type="input" selector="input[name='extension_attributes[reward_points_delta]']"/> <element name="freeShipping" type="select" selector="//select[@name='simple_free_shipping']"/> <!-- Manage Coupon Codes sub-form --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.xml index f3d5e9627efcf..eb4098d71dca2 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.xml @@ -7,8 +7,8 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CartPriceRulesSubmenuSection"> <element name="cartPriceRules" type="button" selector="//li[@data-ui-id='menu-magento-catalogrule-promo']//li[@data-ui-id='menu-magento-salesrule-promo-quote']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml index e14bfb554b3f6..7e2ef0b512020 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml @@ -12,5 +12,6 @@ <element name="CouponInput" type="input" selector="#coupon_code"/> <element name="DiscountInput" type="input" selector="#discount-code"/> <element name="ApplyCodeBtn" type="button" selector="//span[text()='Apply Discount']"/> + <element name="CancelCoupon" type="button" selector="//button[@value='Cancel Coupon']"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml new file mode 100644 index 0000000000000..ab085dc5ae137 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -0,0 +1,126 @@ +<?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="AdminCartRulesAppliedForProductInCartTest"> + <annotations> + <features value="SalesRule"/> + <stories value="The cart rule cannot effect the cart"/> + <title value="Check that cart rules applied for product in cart"/> + <description value="Check that cart rules applied for product in cart"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96722"/> + <useCaseId value="MAGETWO-96410"/> + <group value="SalesRule"/> + </annotations> + + <before> + <!--Create category and product--> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">200</field> + <field key="quantity">500</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + + <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> + + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{PriceRuleWithCondition.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters1"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Start creating a bundle product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <pressKey selector="{{AdminProductFormSection.productSku}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="enter"/> + + <!--Off dynamic price and set value--> + <scrollToTopOfPage stepKey="scrollToTopOfThePageToSeePriceTypeElement"/> + <click selector="{{AdminProductFormBundleSection.dynamicPrice}}" stepKey="offDynamicPrice"/> + <fillField selector="{{AdminProductFormBundleSection.priceField}}" userInput="0" stepKey="setProductPrice"/> + + <!-- Add category to product --> + <click selector="{{AdminProductFormBundleSection.categoriesDropDown}}" stepKey="dropDownCategories"/> + <fillField selector="{{AdminProductFormBundleSection.searchForCategory}}" userInput="$$defaultCategory.name$$" stepKey="searchForCategory"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.selectCategory}}" stepKey="waitForElementLoaded"/> + <click selector="{{AdminProductFormBundleSection.selectCategory}}" stepKey="selectCategory"/> + <click selector="{{AdminProductFormBundleSection.categoriesLabel}}" stepKey="clickOnCategoriesLabelToCloseOptions"/> + + <!-- Add option, a "Radio Buttons" type option, with one product and set fixed price 200--> + <actionGroup ref="addBundleOptionWithOneProduct" stepKey="addBundleOptionWithOneProduct"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct.sku$$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option One"/> + <argument name="inputType" value="radio"/> + </actionGroup> + <selectOption selector="{{AdminProductFormBundleSection.bundlePriceType}}" userInput="Fixed" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductFormBundleSection.bundlePriceValue}}" userInput="200" stepKey="fillPriceValue"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Create cart price rule--> + <actionGroup ref="AdminCreateCartPriceRuleWithConditions" stepKey="createRule"> + <argument name="ruleName" value="PriceRuleWithCondition"/> + <argument name="condition1" value="Products subselection"/> + <argument name="condition2" value="Category"/> + <argument name="ruleToChange1" value="is"/> + <argument name="rule1" value="equals or greater than"/> + <argument name="ruleToChange2" value="..."/> + <argument name="rule2" value="2"/> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> + + <!--Go to Storefront and add product to cart and checkout from cart--> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="2" stepKey="setQuantity"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="AddProductToCard"> + <argument name="productName" value="$$simpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check totals--> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="grabSubtotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="grabShippingTotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="grabTotal"/> + <assertEquals stepKey="assertSubtotal"> + <expectedResult type="string">$400.00</expectedResult> + <actualResult type="variable">$grabSubtotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertShippingTotal"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">$grabShippingTotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertTotal"> + <expectedResult type="string">$410.00</expectedResult> + <actualResult type="variable">$grabTotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 271477070d8cd..7b350c0208cc1 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -53,7 +53,14 @@ <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection"/> <fillField selector="{{AdminCartPriceRulesFormSection.couponQty}}" userInput="10" stepKey="fillCouponQty"/> <click selector="{{AdminCartPriceRulesFormSection.generateCouponsButton}}" stepKey="clickGenerate"/> - <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="10 coupon(s) have been generated." stepKey="seeGenerationSuccess"/> + <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="Message is added to queue, wait to get your coupons soon" stepKey="seeGenerationSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> + <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection2"/> <!-- Grab a coupon code and hold on to it for later --> <grabTextFrom selector="{{AdminCartPriceRulesFormSection.generatedCouponByIndex('1')}}" stepKey="grabCouponCode"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml new file mode 100644 index 0000000000000..f281b0abf87a0 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingCartPriceRulesNavigateMenuTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing cart price rules navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Cart Price Rules"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14143"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingCartPriceRulesPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingPromotionsCartPriceRules.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingPromotionsCartPriceRules.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml new file mode 100644 index 0000000000000..77ee270b3bcaa --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -0,0 +1,17 @@ +<?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="DeleteCustomerGroupTest"> + <actionGroup ref="AdminOpenNewCartPriceRuleFormPageActionGroup" stepKey="openNewCartPriceRuleForm" /> + <actionGroup ref="AssertCustomerGroupNotOnCartPriceRuleFormActionGroup" stepKey="assertCustomerGroupNotOnCartPriceRuleForm"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index da9eb8e19790e..0d365dc089e43 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -29,30 +29,21 @@ <actionGroup ref="StorefrontCheckCouponAppliedActionGroup" stepKey="couponCheckAppliedDiscount" after="couponApplyCoupon"> <argument name="rule" value="$$createSalesRule$$"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="discount" value="E2EB2CQuoteWith10PercentDiscount.discount"/> + <argument name="discount" value="48.00"/> </actionGroup> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="couponCheckCartWithDiscount" after="couponCheckAppliedDiscount"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuoteWith10PercentDiscount.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuoteWith10PercentDiscount.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuoteWith10PercentDiscount.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuoteWith10PercentDiscount.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="447.00"/> </actionGroup> <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <comment userInput="End of using coupon code" stepKey="endOfUsingCouponCode" after="cartAssertCartAfterCancelCoupon" /> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index d735d5a73f0f5..7a995b1feeeda 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -29,30 +29,21 @@ <actionGroup ref="StorefrontCheckCouponAppliedActionGroup" stepKey="couponCheckAppliedDiscount" after="couponApplyCoupon"> <argument name="rule" value="$$createSalesRule$$"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="discount" value="E2EB2CQuoteWith10PercentDiscount.discount"/> + <argument name="discount" value="48.00"/> </actionGroup> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="couponCheckCartWithDiscount" after="couponCheckAppliedDiscount"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuoteWith10PercentDiscount.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuoteWith10PercentDiscount.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuoteWith10PercentDiscount.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuoteWith10PercentDiscount.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="447.00"/> </actionGroup> <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <comment userInput="End of using coupon code" stepKey="endOfUsingCouponCode" after="cartAssertCartAfterCancelCoupon"/> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index ed05f8b27e5ca..84537fb69ed41 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -56,8 +56,19 @@ stepKey="clickManageCouponCodes"/> <fillField selector="{{AdminCartPriceRulesFormSection.couponQty}}" userInput="1" stepKey="fillFieldCouponQty"/> <click selector="{{AdminCartPriceRulesFormSection.generateCouponsButton}}" stepKey="clickGenerateCoupon"/> - <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="1 coupon(s) have been generated." + <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="Message is added to queue, wait to get your coupons soon" stepKey="seeSuccessMessage"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" + dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" + stepKey="clickManageCouponCodes2"/> + + <!-- Grab a coupon code and hold on to it for later --> <grabTextFrom selector="{{AdminCartPriceRulesFormSection.generatedCouponByIndex('1')}}" stepKey="couponCode"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml new file mode 100644 index 0000000000000..d8c5b42dbaaaf --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml @@ -0,0 +1,116 @@ +<?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="StorefrontCategoryRulesShouldApplyToComplexProductsTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create cart price rule"/> + <title value="Category rules should apply to complex products"/> + <description value="Sales rules filtering on category should apply to all products, including complex products."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-70192"/> + <group value="catalogRule"/> + </annotations> + <before> + <!-- Create two Categories: CAT1 and CAT2 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleSubCategory" stepKey="createCategory2"/> + <!--Create config1 and config2--> + <actionGroup ref="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" stepKey="createConfigurableProduct1"> + <argument name="productName" value="config1"/> + </actionGroup> + <actionGroup ref="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" stepKey="createConfigurableProduct2"> + <argument name="productName" value="config2"/> + </actionGroup> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Assign config1 and the associated child products to CAT1 --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignConfigurableProduct1ToCategory"> + <argument name="productId" value="$$createConfigProductCreateConfigurableProduct1.id$$"/> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignConfig1ChildProduct1ToCategory"> + <argument name="productId" value="$$createConfigChildProduct1CreateConfigurableProduct1.id$$"/> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignConfig1ChildProduct2ToCategory"> + <argument name="productId" value="$$createConfigChildProduct2CreateConfigurableProduct1.id$$"/> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <!-- Assign config12 and the associated child products to CAT2 --> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignConfigurableProduct2ToCategory2"> + <argument name="productId" value="$$createConfigProductCreateConfigurableProduct2.id$$"/> + <argument name="categoryName" value="$$createCategory2.name$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignConfig2ChildProduct1ToCategory2"> + <argument name="productId" value="$$createConfigChildProduct1CreateConfigurableProduct2.id$$"/> + <argument name="categoryName" value="$$createCategory2.name$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductToCategory" stepKey="assignConfig2ChildProduct2ToCategory2"> + <argument name="productId" value="$$createConfigChildProduct2CreateConfigurableProduct2.id$$"/> + <argument name="categoryName" value="$$createCategory2.name$$"/> + </actionGroup> + </before> + <after> + <!--Delete configurable product 1--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct1" stepKey="deleteConfigProduct1"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory1"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct1" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct1" stepKey="deleteConfigProductAttribute1"/> + <!--Delete configurable product 2--> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct2" stepKey="deleteConfigProduct2"/> + <deleteData createDataKey="createCategory2" stepKey="deleteCategory2"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct2" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct2" stepKey="deleteConfigChildProduct4"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct2" stepKey="deleteConfigProductAttribute2"/> + <!--Delete Cart Price Rule --> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- 1: Create a cart price rule applying to CAT1 with discount --> + <createData entity="SalesRuleNoCouponWithFixedDiscount" stepKey="createCartPriceRule"/> + <amOnPage url="{{AdminCartPriceRuleEditPage.url($$createCartPriceRule.rule_id$$)}}" stepKey="goToCartPriceRuleEditPage"/> + <actionGroup ref="SetConditionForActionsInCartPriceRuleActionGroup" stepKey="setConditionForActionsInCartPriceRuleActionGroup"> + <argument name="actionValue" value="$$createCategory.id$$"/> + </actionGroup> + <!-- 2: Go to frontend and add an item from both CAT1 and CAT2 to your cart --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontend"/> + <!-- 3: Open configurable product 1 and add all his child products to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct1.custom_attributes[url_key]$$)}}" stepKey="amOnConfigurableProductPage"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttributeCreateConfigurableProduct1.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct1.option[store_labels][0][label]$$" stepKey="selectOption"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddConfigurableProductToCart"> + <argument name="product" value="$$createConfigProductCreateConfigurableProduct1$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttributeCreateConfigurableProduct1.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption2CreateConfigurableProduct1.option[store_labels][0][label]$$" stepKey="selectOption2"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddConfigurableProductToCart2"> + <argument name="product" value="$$createConfigProductCreateConfigurableProduct1$$"/> + <argument name="productCount" value="2"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToCart"/> + <!-- Discount amount is not applied --> + <dontSee selector="{{CheckoutCartSummarySection.discountLabel}}" stepKey="discountIsNotApply"/> + <!-- 3: Open configurable product 2 and add all his child products to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct2.custom_attributes[url_key]$$)}}" stepKey="amOnConfigurableProductPage2"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttributeCreateConfigurableProduct2.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct2.option[store_labels][0][label]$$" stepKey="selectOption3"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddConfigurableProductToCart3"> + <argument name="product" value="$$createConfigProductCreateConfigurableProduct2$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttributeCreateConfigurableProduct2.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption2CreateConfigurableProduct2.option[store_labels][0][label]$$" stepKey="selectOption4"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddConfigurableProductToCart4"> + <argument name="product" value="$$createConfigProductCreateConfigurableProduct2$$"/> + <argument name="productCount" value="4"/> + </actionGroup> + <!-- Discount amount is applied --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToCart2"/> + <see selector="{{CheckoutCartSummarySection.discountTotal}}" userInput="-$100.00" stepKey="discountIsApply"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php b/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php index 2ef77d72a8af5..66970f28598b6 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Controller/Adminhtml/Promo/Quote/GenerateTest.php @@ -8,6 +8,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\SalesRule\Model\CouponGenerator; +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -50,6 +51,9 @@ class GenerateTest extends \PHPUnit\Framework\TestCase /** @var CouponGenerator | \PHPUnit_Framework_MockObject_MockObject */ private $couponGenerator; + /** @var CouponGenerationSpecInterfaceFactory | \PHPUnit_Framework_MockObject_MockObject */ + private $couponGenerationSpec; + /** * Test setup */ @@ -98,6 +102,9 @@ protected function setUp() $this->couponGenerator = $this->getMockBuilder(CouponGenerator::class) ->disableOriginalConstructor() ->getMock(); + $this->couponGenerationSpec = $this->getMockBuilder(CouponGenerationSpecInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->model = $this->objectManagerHelper->getObject( @@ -107,7 +114,8 @@ protected function setUp() 'coreRegistry' => $this->registryMock, 'fileFactory' => $this->fileFactoryMock, 'dateFilter' => $this->dateMock, - 'couponGenerator' => $this->couponGenerator + 'couponGenerator' => $this->couponGenerator, + 'generationSpecFactory' => $this->couponGenerationSpec ] ); } @@ -144,9 +152,10 @@ public function testExecute() $this->requestMock->expects($this->once()) ->method('getParams') ->willReturn($requestData); - $this->couponGenerator->expects($this->once()) - ->method('generateCodes') - ->with($requestData) + $requestData['quantity'] = isset($requestData['qty']) ? $requestData['qty'] : null; + $this->couponGenerationSpec->expects($this->once()) + ->method('create') + ->with(['data' => $requestData]) ->willReturn(['some_data', 'some_data_2']); $this->messageManager->expects($this->once()) ->method('addSuccessMessage'); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 42448565791c5..e86068946ca78 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -5,6 +5,13 @@ */ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Validator; +use Magento\Store\Model\Store; +use PHPUnit\Framework\MockObject\MockObject; + /** * Class ValidatorTest * @@SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -17,50 +24,55 @@ class ValidatorTest extends \PHPUnit\Framework\TestCase protected $helper; /** - * @var \Magento\SalesRule\Model\Validator + * @var Validator */ protected $model; /** - * @var \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Item|MockObject */ protected $item; /** - * @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Address|MockObject */ protected $addressMock; /** - * @var \Magento\SalesRule\Model\RulesApplier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\RulesApplier|MockObject */ protected $rulesApplier; /** - * @var \Magento\SalesRule\Model\Validator\Pool|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Validator\Pool|MockObject */ protected $validators; /** - * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Utility|MockObject */ protected $utility; /** - * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|MockObject */ protected $ruleCollection; /** - * @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Data|MockObject */ protected $catalogData; /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Message\ManagerInterface|MockObject */ protected $messageManager; + /** + * @var PriceCurrencyInterface|MockObject + */ + private $priceCurrency; + protected function setUp() { $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -74,6 +86,7 @@ protected function setUp() ->setMethods( [ 'getShippingAmountForDiscount', + 'getBaseShippingAmountForDiscount', 'getQuote', 'getCustomAttributesCodes', 'setCartFixedRules' @@ -81,7 +94,7 @@ protected function setUp() ) ->getMock(); - /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|MockObject $item */ $this->item = $this->createPartialMock( \Magento\Quote\Model\Quote\Item::class, ['__wakeup', 'getAddress', 'getParentItemId'] @@ -100,10 +113,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $ruleCollectionFactoryMock = $this->prepareRuleCollectionMock($this->ruleCollection); + $this->priceCurrency = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMock(); - /** @var \Magento\SalesRule\Model\Validator|\PHPUnit_Framework_MockObject_MockObject $validator */ + /** @var Validator|MockObject $validator */ $this->model = $this->helper->getObject( - \Magento\SalesRule\Model\Validator::class, + Validator::class, [ 'context' => $context, 'registry' => $registry, @@ -112,7 +128,8 @@ protected function setUp() 'utility' => $this->utility, 'rulesApplier' => $this->rulesApplier, 'validators' => $this->validators, - 'messageManager' => $this->messageManager + 'messageManager' => $this->messageManager, + 'priceCurrency' => $this->priceCurrency ] ); $this->model->setWebsiteId(1); @@ -131,7 +148,7 @@ protected function setUp() } /** - * @return \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Quote\Model\Quote\Item|MockObject */ protected function getQuoteItemMock() { @@ -145,8 +162,8 @@ protected function getQuoteItemMock() $itemSimple = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['getAddress', '__wakeup']); $itemSimple->expects($this->any())->method('getAddress')->will($this->returnValue($this->addressMock)); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStoreId', '__wakeup']); + /** @var $quote Quote */ + $quote = $this->createPartialMock(Quote::class, ['getStoreId', '__wakeup']); $quote->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); $itemData = include $fixturePath . 'quote_item_downloadable.php'; @@ -168,7 +185,7 @@ public function testCanApplyRules() $this->model->getCouponCode() ); $item = $this->getQuoteItemMock(); - $rule = $this->createMock(\Magento\SalesRule\Model\Rule::class); + $rule = $this->createMock(Rule::class); $actionsCollection = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']); $actionsCollection->expects($this->any()) ->method('validate') @@ -278,7 +295,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected() public function testInit() { $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), @@ -314,7 +331,7 @@ public function testCanApplyDiscount() public function testInitTotalsCanApplyDiscount() { $rule = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getSimpleAction', 'getActions', 'getId'] ); $item1 = $this->getMockForAbstractClass( @@ -337,7 +354,7 @@ public function testInitTotalsCanApplyDiscount() $rule->expects($this->any()) ->method('getSimpleAction') - ->willReturn(\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION); + ->willReturn(Rule::CART_FIXED_ACTION); $iterator = new \ArrayIterator([$rule]); $this->ruleCollection->expects($this->once())->method('getIterator')->willReturn($iterator); $validator = $this->getMockBuilder(\Magento\Framework\Validator\AbstractValidator::class) @@ -392,7 +409,7 @@ public function testInitTotalsNoItems() /** * @param $ruleCollection - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function prepareRuleCollectionMock($ruleCollection) { @@ -427,14 +444,14 @@ public function testProcessShippingAmountNoRules() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } public function testProcessShippingAmountProcessDisabled() { - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); @@ -448,51 +465,54 @@ public function testProcessShippingAmountProcessDisabled() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } /** + * Tests shipping amounts according to rule simple action. + * * @param string $action + * @param int $ruleDiscount + * @param int $shippingDiscount * @dataProvider dataProviderActions */ - public function testProcessShippingAmountActions($action) + public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount): void { - $discountAmount = 50; + $shippingAmount = 5; - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) ->getMock(); - $ruleMock->expects($this->any()) - ->method('getApplyToShipping') + $ruleMock->method('getApplyToShipping') ->willReturn(true); - $ruleMock->expects($this->any()) - ->method('getDiscountAmount') - ->willReturn($discountAmount); - $ruleMock->expects($this->any()) - ->method('getSimpleAction') + $ruleMock->method('getDiscountAmount') + ->willReturn($ruleDiscount); + $ruleMock->method('getSimpleAction') ->willReturn($action); $iterator = new \ArrayIterator([$ruleMock]); - $this->ruleCollection->expects($this->any()) - ->method('getIterator') + $this->ruleCollection->method('getIterator') ->willReturn($iterator); - $this->utility->expects($this->any()) - ->method('canProcessRule') + $this->utility->method('canProcessRule') ->willReturn(true); + $this->priceCurrency->method('convert') + ->willReturn($ruleDiscount); + $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, - $this->model->processShippingAmount($this->setupAddressMock(5)) - ); + + $addressMock = $this->setupAddressMock($shippingAmount); + + self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); + self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); } /** @@ -501,44 +521,48 @@ public function testProcessShippingAmountActions($action) public static function dataProviderActions() { return [ - [\Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::TO_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::BY_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION], + [Rule::TO_PERCENT_ACTION, 50, 2.5], + [Rule::BY_PERCENT_ACTION, 50, 2.5], + [Rule::TO_FIXED_ACTION, 5, 0], + [Rule::BY_FIXED_ACTION, 5, 5], + [Rule::CART_FIXED_ACTION, 5, 0], ]; } /** * @param null|int $shippingAmount - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function setupAddressMock($shippingAmount = null) { - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['setAppliedRuleIds', 'getStore']) ->getMock(); - $quoteMock->expects($this->any()) - ->method('getStore') + + $quoteMock->method('getStore') ->willReturn($storeMock); - $quoteMock->expects($this->any()) - ->method('setAppliedRuleIds') + + $quoteMock->method('setAppliedRuleIds') ->willReturnSelf(); - $this->addressMock->expects($this->any()) - ->method('getShippingAmountForDiscount') + $this->addressMock->method('getShippingAmountForDiscount') ->willReturn($shippingAmount); - $this->addressMock->expects($this->any()) - ->method('getQuote') + + $this->addressMock->method('getBaseShippingAmountForDiscount') + ->willReturn($shippingAmount); + + $this->addressMock->method('getQuote') ->willReturn($quoteMock); - $this->addressMock->expects($this->any()) - ->method('getCustomAttributesCodes') + + $this->addressMock->method('getCustomAttributesCodes') ->willReturn([]); + return $this->addressMock; } @@ -546,7 +570,7 @@ public function testReset() { $this->utility->expects($this->once()) ->method('resetRoundingDeltas'); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); $addressMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) @@ -560,6 +584,6 @@ public function testReset() $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf(\Magento\SalesRule\Model\Validator::class, $this->model->reset($addressMock)); + $this->assertInstanceOf(Validator::class, $this->model->reset($addressMock)); } } diff --git a/app/code/Magento/SalesRule/etc/communication.xml b/app/code/Magento/SalesRule/etc/communication.xml new file mode 100644 index 0000000000000..4c905fa83e2fd --- /dev/null +++ b/app/code/Magento/SalesRule/etc/communication.xml @@ -0,0 +1,12 @@ +<?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:Communication/etc/communication.xsd"> + <topic name="sales_rule.codegenerator" request="Magento\SalesRule\Api\Data\CouponGenerationSpecInterface"> + <handler name="codegeneratorProcessor" type="Magento\SalesRule\Model\Coupon\Consumer" method="process" /> + </topic> +</config> diff --git a/app/code/Magento/SalesRule/etc/db_schema.xml b/app/code/Magento/SalesRule/etc/db_schema.xml index 8dbdf76387cd8..c7427e49219b5 100644 --- a/app/code/Magento/SalesRule/etc/db_schema.xml +++ b/app/code/Magento/SalesRule/etc/db_schema.xml @@ -208,17 +208,17 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Coupon Uses"/> - <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="total_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount"/> - <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount Actual"/> <column xsi:type="decimal" name="discount_amount_actual" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount Actual"/> - <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount Actual"/> <column xsi:type="varchar" name="rule_name" nullable="true" length="255" comment="Rule Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -250,17 +250,17 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Coupon Uses"/> - <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="total_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount"/> - <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount Actual"/> <column xsi:type="decimal" name="discount_amount_actual" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount Actual"/> - <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount Actual"/> <column xsi:type="varchar" name="rule_name" nullable="true" length="255" comment="Rule Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -292,11 +292,11 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Coupon Uses"/> - <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="total_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount"/> <column xsi:type="varchar" name="rule_name" nullable="true" length="255" comment="Rule Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index a8c350457a5a6..27c9a41503b22 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -179,7 +179,7 @@ </arguments> </type> - <type name="\Magento\Quote\Model\Cart\CartTotalRepository"> + <type name="Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="coupon_label_plugin" type="Magento\SalesRule\Plugin\CartTotalRepository" /> </type> </config> diff --git a/app/code/Magento/SalesRule/etc/queue.xml b/app/code/Magento/SalesRule/etc/queue.xml new file mode 100644 index 0000000000000..8217a0b9f6c1a --- /dev/null +++ b/app/code/Magento/SalesRule/etc/queue.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/queue.xsd"> + <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> +</config> diff --git a/app/code/Magento/SalesRule/etc/queue_consumer.xml b/app/code/Magento/SalesRule/etc/queue_consumer.xml new file mode 100644 index 0000000000000..9eb585f48e8e3 --- /dev/null +++ b/app/code/Magento/SalesRule/etc/queue_consumer.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-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" /> +</config> diff --git a/app/code/Magento/SalesRule/etc/queue_publisher.xml b/app/code/Magento/SalesRule/etc/queue_publisher.xml new file mode 100644 index 0000000000000..0863fba2307c5 --- /dev/null +++ b/app/code/Magento/SalesRule/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/publisher.xsd"> + <publisher topic="sales_rule.codegenerator"> + <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 new file mode 100644 index 0000000000000..fd6a9bf36721c --- /dev/null +++ b/app/code/Magento/SalesRule/etc/queue_topology.xml @@ -0,0 +1,12 @@ +<?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-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="codegeneratorBinding" topic="sales_rule.codegenerator" destinationType="queue" destination="codegenerator"/> + </exchange> +</config> diff --git a/app/code/Magento/SalesRule/etc/sales.xml b/app/code/Magento/SalesRule/etc/sales.xml index 3ab197d40b0df..d2db664224873 100644 --- a/app/code/Magento/SalesRule/etc/sales.xml +++ b/app/code/Magento/SalesRule/etc/sales.xml @@ -8,7 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd"> <section name="quote"> <group name="totals"> - <item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="400"/> + <item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="300"/> + <item name="shipping_discount" instance="Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount" sort_order="400"/> </group> </section> </config> diff --git a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml index 9b579f47759a6..570eb0bf151f0 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml @@ -452,7 +452,7 @@ <dataScope>discount_step</dataScope> </settings> </field> - <field name="apply_to_shipping" component="Magento_Ui/js/form/element/single-checkbox-toggle-notice" formElement="checkbox"> + <field name="apply_to_shipping" component="Magento_SalesRule/js/form/element/apply_to_shipping" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">sales_rule</item> diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js new file mode 100644 index 0000000000000..dfb3f909345b3 --- /dev/null +++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js @@ -0,0 +1,37 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/single-checkbox-toggle-notice' +], function (Checkbox) { + 'use strict'; + + return Checkbox.extend({ + defaults: { + imports: { + toggleDisabled: '${ $.parentName }.simple_action:value' + } + }, + + /** + * Toggle element disabled state according to simple action value. + * + * @param {String} action + */ + toggleDisabled: function (action) { + switch (action) { + case 'cart_fixed': + this.disabled(true); + break; + default: + this.disabled(false); + } + + if (this.disabled()) { + this.checked(false); + } + } + }); +}); diff --git a/app/code/Magento/SalesSequence/etc/db_schema.xml b/app/code/Magento/SalesSequence/etc/db_schema.xml index 0e580b85bd608..7ad48badf7b80 100644 --- a/app/code/Magento/SalesSequence/etc/db_schema.xml +++ b/app/code/Magento/SalesSequence/etc/db_schema.xml @@ -41,7 +41,7 @@ <column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Prefix"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> - <column xsi:type="varchar" name="sequence_table" nullable="false" length="32" comment="table for sequence"/> + <column xsi:type="varchar" name="sequence_table" nullable="false" length="64" comment="table for sequence"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="meta_id"/> </constraint> diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php index f2770f77cc533..c32ca04d39b61 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php @@ -6,15 +6,17 @@ namespace Magento\Search\Controller\Adminhtml\Synonyms; +use Magento\Framework\App\Action\HttpPostActionInterface; + /** - * Mass-Delete Controller + * Mass-Delete Controller. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class MassDelete extends \Magento\Backend\App\Action +class MassDelete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** - * Authorization level of a basic admin session + * Authorization level of a basic admin session. * * @see _isAllowed() */ @@ -56,7 +58,7 @@ public function __construct( } /** - * Execute action + * Execute action. * * @return \Magento\Backend\Model\View\Result\Redirect * @throws \Magento\Framework\Exception\LocalizedException|\Exception diff --git a/app/code/Magento/Search/Model/EngineResolver.php b/app/code/Magento/Search/Model/EngineResolver.php index 720df0e0fda97..9e4ebf5436359 100644 --- a/app/code/Magento/Search/Model/EngineResolver.php +++ b/app/code/Magento/Search/Model/EngineResolver.php @@ -10,6 +10,8 @@ use Psr\Log\LoggerInterface; /** + * Search engine resolver model. + * * @api * @since 100.1.0 */ @@ -61,6 +63,7 @@ class EngineResolver implements EngineResolverInterface /** * @param ScopeConfigInterface $scopeConfig * @param array $engines + * @param LoggerInterface $logger * @param string $path * @param string $scopeType * @param string $scopeCode diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 46e794a1954cf..45eee0a4001d1 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -87,7 +87,7 @@ private function queryByPhrase($phrase) { $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], - $phrase, + $this->escapePhrase($phrase), Fulltext::FULLTEXT_MODE_BOOLEAN ); $query = $this->getConnection()->select()->from( @@ -97,6 +97,18 @@ private function queryByPhrase($phrase) return $this->getConnection()->fetchAll($query); } + /** + * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. + * + * @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html + * @param string $phrase + * @return string + */ + private function escapePhrase(string $phrase): string + { + return preg_replace('/@+|[@+-]+$/', '', $phrase); + } + /** * A private helper function to retrieve matching synonym groups per scope * diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml new file mode 100644 index 0000000000000..e0b3d4b850bbb --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml @@ -0,0 +1,30 @@ +<?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"> + <!-- Filter by search query and select --> + <actionGroup name="searchTermFilterBySearchQuery"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForResetFilter"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultLoad"/> + <checkOption selector="{{AdminCatalogSearchTermIndexSection.searchTermRowCheckboxBySearchQuery(searchQuery)}}" stepKey="checkCheckBox"/> + </actionGroup> + + <!-- Delete search term --> + <actionGroup name="deleteSearchTerm"> + <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> + <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> + <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> + <waitForElementVisible selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml new file mode 100644 index 0000000000000..1518adad01347 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.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="SearchTerm" type="searchTerm"> + <data key="query_text" unique="suffix">Query text</data> + <data key="store_id">1</data> + <data key="redirect" unique="suffix">http://example.com/</data> + <data key="display_in_terms">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml b/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml new file mode 100644 index 0000000000000..0bd2dc9be4855 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.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="CreateSearchTerm" dataType="searchTerm" type="create" auth="adminFormKey" url="/search/term/save/" method="POST"> + <field key="query_text">string</field> + <field key="store_id">integer</field> + <field key="redirect">string</field> + <field key="display_in_terms">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml new file mode 100644 index 0000000000000..0bb929d889351 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml @@ -0,0 +1,25 @@ +<?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="StorefrontQuickSearchResultsSection"> + <element name="searchTextBox" type="text" selector="#search"/> + <element name="searchTextBoxButton" type="button" selector="button[class='action search']"/> + <element name="allResults" type="block" selector=".column.main"/> + <element name="productLink" type="select" selector="a[class='product-item-link']"/> + <element name="productByIndex" type="button" selector=".product-items li:nth-child({{var}}) .product-item-info" parameterized="true"/> + <element name="productByName" type="button" selector="//div[contains(@class, 'product-item-info') and .//*[contains(., '{{var}}')]]" parameterized="true"/> + <element name="addToCartBtn" type="button" selector="//button[contains(@class, 'tocart')]"/> + <element name="messageSection" type="text" selector="div .message"/> + <element name="productSpecialPrice" type="text" selector="//a[contains(text(), '{{productName}}')]/ancestor::div//span[contains(@data-price-type, 'finalPrice')]/span[contains(@class, 'price')]" parameterized="true"/> + <element name="asLowAsLabel" type="text" selector=".minimal-price-link > span"/> + <element name="textArea" type="text" selector="li[class='item']"/> + <element name="regularPrice" type="text" selector="//span[@class='price-wrapper ']/span[@class='price']"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml index 2b08e9b4b85ec..3c2909b59c0de 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontQuickSearchSection"> <element name="searchPhrase" type="input" selector="#search"/> <element name="searchButton" type="button" selector="button.action.search" timeout="30"/> + <element name="searchMiniForm" type="input" selector="#search_mini_form"/> </section> </sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml new file mode 100644 index 0000000000000..67ccb51bf401e --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml @@ -0,0 +1,83 @@ +<?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="AdminMassDeleteSearchTermEntityTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Delete search term"/> + <title value="Admin mass delete search term entity test"/> + <description value="Admin should be able to Mass Delete Search Term Entity Test"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14767"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create three search term --> + <createData entity="SearchTerm" stepKey="createFirstSearchTerm"/> + <createData entity="SearchTerm" stepKey="createSecondSearchTerm"/> + <createData entity="SearchTerm" stepKey="createThirdSearchTerm"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to the catalog search term page --> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + + <!-- Select all created below search terms --> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByFirstSearchQuery"> + <argument name="searchQuery" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterBySecondSearchQuery"> + <argument name="searchQuery" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByThirdSearchQuery"> + <argument name="searchQuery" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + + <!-- Delete created below search terms --> + <actionGroup ref="deleteSearchTerm" stepKey="deleteSearchTerms"/> + + <!-- Assert search terms are absent on the search term page --> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertFirstSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertSecondSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertThirdSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + + <!-- Go to storefront page --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + + <!-- Verify search term deletion on storefront --> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForFirstSearchTerm"> + <argument name="phrase" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForFirstSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForSecondSearchTerm"> + <argument name="phrase" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForSecondSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForThirdSearchTerm"> + <argument name="phrase" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForThirdSearchTerm"/> + </test> +</tests> diff --git a/app/code/Magento/Search/i18n/de_DE.csv b/app/code/Magento/Search/i18n/de_DE.csv deleted file mode 100644 index 8b4b04aa3b9ec..0000000000000 --- a/app/code/Magento/Search/i18n/de_DE.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/es_ES.csv b/app/code/Magento/Search/i18n/es_ES.csv deleted file mode 100644 index 8b4b04aa3b9ec..0000000000000 --- a/app/code/Magento/Search/i18n/es_ES.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/fr_FR.csv b/app/code/Magento/Search/i18n/fr_FR.csv deleted file mode 100644 index 8b4b04aa3b9ec..0000000000000 --- a/app/code/Magento/Search/i18n/fr_FR.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/nl_NL.csv b/app/code/Magento/Search/i18n/nl_NL.csv deleted file mode 100644 index 8b4b04aa3b9ec..0000000000000 --- a/app/code/Magento/Search/i18n/nl_NL.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/pt_BR.csv b/app/code/Magento/Search/i18n/pt_BR.csv deleted file mode 100644 index c10566a7c9800..0000000000000 --- a/app/code/Magento/Search/i18n/pt_BR.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Mecanismo de Busca" diff --git a/app/code/Magento/Search/i18n/zh_Hans_CN.csv b/app/code/Magento/Search/i18n/zh_Hans_CN.csv deleted file mode 100644 index 8b4b04aa3b9ec..0000000000000 --- a/app/code/Magento/Search/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" 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 2ea87be13d5e3..a98a70a90cede 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -16,34 +16,41 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); <div class="block block-content"> <form class="form minisearch" id="search_mini_form" action="<?= /* @escapeNotVerified */ $helper->getResultUrl() ?>" method="get"> <div class="field search"> - <label class="label" for="search" data-role="minisearch-label"> - <span><?= /* @escapeNotVerified */ __('Search') ?></span> - </label> <div class="control"> - <input id="search" - data-mage-init='{"quickSearch":{ + <label for="search" data-role="minisearch-label"> + <span class="label"> + <?= /* @escapeNotVerified */ __('Search') ?> + </span> + <input + aria-expanded="false" + id="search" + data-mage-init='{"quickSearch":{ "formSelector":"#search_mini_form", "url":"<?= /* @escapeNotVerified */ $helper->getSuggestUrl()?>", "destinationSelector":"#search_autocomplete"} - }' - type="text" - name="<?= /* @escapeNotVerified */ $helper->getQueryParamName() ?>" - value="<?= /* @escapeNotVerified */ $helper->getEscapedQueryText() ?>" - placeholder="<?= /* @escapeNotVerified */ __('Search entire store here...') ?>" - class="input-text" - maxlength="<?= /* @escapeNotVerified */ $helper->getMaxQueryLength() ?>" - role="combobox" - aria-haspopup="false" - aria-autocomplete="both" - autocomplete="off"/> + }' + type="text" + name="<?= /* @escapeNotVerified */ $helper->getQueryParamName() ?>" + value="<?= /* @escapeNotVerified */ $helper->getEscapedQueryText() ?>" + placeholder="<?= /* @escapeNotVerified */ __('Search entire store here...') ?>" + class="input-text" + maxlength="<?= /* @escapeNotVerified */ $helper->getMaxQueryLength() ?>" + role="combobox" + aria-haspopup="false" + aria-autocomplete="both" + autocomplete="off" + /> + </label> <div id="search_autocomplete" class="search-autocomplete"></div> <?= $block->getChildHtml() ?> </div> </div> <div class="actions"> <button type="submit" - title="<?= $block->escapeHtml(__('Search')) ?>" - class="action search"> + title="<?= $block->escapeHtml(__('Search')) ?>" + class="action search" + aria-label="Search" + > <span><?= /* @escapeNotVerified */ __('Search') ?></span> </button> </div> 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 15bcf2e73393e..5331ba3b447ac 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 @@ -72,7 +72,6 @@ define([ }.bind(this), exit: function () { this.isExpandable = false; - this.element.removeAttr('aria-expanded'); }.bind(this) }); diff --git a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php index 9d86b55158be5..5d72ba261f316 100644 --- a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php +++ b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php @@ -48,13 +48,13 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function check($securityEventType, $accountReference = null, $longIp = null) { $isEnabled = $this->securityConfig->getPasswordResetProtectionType() != ResetMethod::OPTION_NONE; $allowedAttemptsNumber = $this->securityConfig->getMaxNumberPasswordResetRequests(); - if ($isEnabled and $allowedAttemptsNumber) { + if ($isEnabled && $allowedAttemptsNumber) { $collection = $this->prepareCollection($securityEventType, $accountReference, $longIp); if ($collection->count() >= $allowedAttemptsNumber) { throw new SecurityViolationException( diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenCreatingNewUserTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenCreatingNewUserTest.xml new file mode 100644 index 0000000000000..4ceffd676313d --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenCreatingNewUserTest.xml @@ -0,0 +1,79 @@ +<?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="AdminUserLockWhenCreatingNewUserTest"> + <annotations> + <features value="Security"/> + <stories value="Runs Lock admin user when creating new user test."/> + <title value="Lock admin user when creating new user"/> + <description value="Runs Lock admin user when creating new user test."/> + <testCaseId value="MC-14383" /> + <severity value="CRITICAL"/> + <group value="security"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Log in to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Unlock Admin user --> + <magentoCLI command="admin:user:unlock {{DefaultAdminUser.username}}" stepKey="unlockAdminUser"/> + </after> + + <!-- Open Admin New User Page --> + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + + <!-- Perform add new admin user 6 specified number of times. + "The password entered for the current user is invalid. Verify the password and try again." appears after each attempt.--> + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserFirstAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveFirstAttempt" /> + <actionGroup ref="AssertAdminUserSaveMessageActionGroup" stepKey="seeInvalidPasswordError"> + <argument name="message" value="The password entered for the current user is invalid. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserSecondAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveSecondAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserThirdAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveThirdAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserFourthAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveFourthAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserFifthAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveFifthAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserSixthAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveSixthAttempt" /> + + <!-- Check Error that account has been locked --> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeLockUserErrorMessage"> + <argument name="message" value="Your account is temporarily disabled. Please try again later." /> + </actionGroup> + + <!-- Try to login as admin and check error --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsLockedAdmin"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeLoginUserErrorMessage" /> + </test> +</tests> diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 43e95ebe43d48..1c4b550361359 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -5,6 +5,7 @@ */ namespace Magento\SendFriend\Block; +use Magento\Captcha\Block\Captcha; use Magento\Customer\Model\Context; /** @@ -170,6 +171,7 @@ public function setFormData($data) /** * Retrieve Current Product Id * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getProductId() @@ -180,6 +182,7 @@ public function getProductId() /** * Retrieve current category id for product * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getCategoryId() @@ -222,4 +225,24 @@ public function canSend() { return !$this->sendfriend->isExceedLimit(); } + + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'product_sendtofriend_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + } } diff --git a/app/code/Magento/SendFriend/Controller/Product/Send.php b/app/code/Magento/SendFriend/Controller/Product/Send.php index 8b0ae8dcf4383..d9fef595ce2a3 100644 --- a/app/code/Magento/SendFriend/Controller/Product/Send.php +++ b/app/code/Magento/SendFriend/Controller/Product/Send.php @@ -5,9 +5,13 @@ */ namespace Magento\SendFriend\Controller\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class Send extends \Magento\SendFriend\Controller\Product +/** + * Controller class. Represents rendering and request flow + */ +class Send extends \Magento\SendFriend\Controller\Product implements HttpGetActionInterface { /** * @var \Magento\Catalog\Model\Session diff --git a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php index 4b1f724cb83a6..696c235899370 100644 --- a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php +++ b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php @@ -6,10 +6,18 @@ namespace Magento\SendFriend\Controller\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; +use Magento\SendFriend\Model\CaptchaValidator; -class Sendmail extends \Magento\SendFriend\Controller\Product +/** + * Class Sendmail. Represents request flow logic of 'sendmail' feature + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Sendmail extends \Magento\SendFriend\Controller\Product implements HttpPostActionInterface { /** * @var \Magento\Catalog\Api\CategoryRepositoryInterface @@ -22,6 +30,13 @@ class Sendmail extends \Magento\SendFriend\Controller\Product protected $catalogSession; /** + * @var CaptchaValidator + */ + private $captchaValidator; + + /** + * Sendmail class construct + * * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator @@ -29,6 +44,7 @@ class Sendmail extends \Magento\SendFriend\Controller\Product * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository * @param \Magento\Catalog\Model\Session $catalogSession + * @param CaptchaValidator|null $captchaValidator */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -37,11 +53,13 @@ public function __construct( \Magento\SendFriend\Model\SendFriend $sendFriend, \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository, - \Magento\Catalog\Model\Session $catalogSession + \Magento\Catalog\Model\Session $catalogSession, + CaptchaValidator $captchaValidator = null ) { parent::__construct($context, $coreRegistry, $formKeyValidator, $sendFriend, $productRepository); $this->categoryRepository = $categoryRepository; $this->catalogSession = $catalogSession; + $this->captchaValidator = $captchaValidator ?: ObjectManager::getInstance()->create(CaptchaValidator::class); } /** @@ -49,17 +67,13 @@ public function __construct( * * @return \Magento\Framework\Controller\ResultInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->_formKeyValidator->validate($this->getRequest())) { - $resultRedirect->setPath('sendfriend/product/send', ['_current' => true]); - return $resultRedirect; - } - $product = $this->_initProduct(); $data = $this->getRequest()->getPostValue(); @@ -89,6 +103,9 @@ public function execute() try { $validate = $this->sendFriend->validate(); + + $this->captchaValidator->validateSending($this->getRequest()); + if ($validate === true) { $this->sendFriend->send(); $this->messageManager->addSuccess(__('The link to a friend was sent.')); diff --git a/app/code/Magento/SendFriend/Model/CaptchaValidator.php b/app/code/Magento/SendFriend/Model/CaptchaValidator.php new file mode 100644 index 0000000000000..11fbbdf72f6db --- /dev/null +++ b/app/code/Magento/SendFriend/Model/CaptchaValidator.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SendFriend\Model; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Class CaptchaValidator. Performs captcha validation + */ +class CaptchaValidator +{ + /** + * @var Data + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + + /** + * @var UserContextInterface + */ + private $currentUser; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * CaptchaValidator constructor. + * + * @param Data $captchaHelper + * @param CaptchaStringResolver $captchaStringResolver + * @param UserContextInterface $currentUser + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + Data $captchaHelper, + CaptchaStringResolver $captchaStringResolver, + UserContextInterface $currentUser, + CustomerRepositoryInterface $customerRepository + ) { + $this->captchaHelper = $captchaHelper; + $this->captchaStringResolver = $captchaStringResolver; + $this->currentUser = $currentUser; + $this->customerRepository = $customerRepository; + } + + /** + * Entry point for captcha validation + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function validateSending(RequestInterface $request): void + { + $this->validateCaptcha($request); + } + + /** + * Validates captcha and triggers log attempt + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function validateCaptcha(RequestInterface $request): void + { + $captchaTargetFormName = 'product_sendtofriend_form'; + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaTargetFormName); + + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $request, + $captchaTargetFormName + ); + + $isCorrectCaptcha = $captchaModel->isCorrect($word); + + if (!$isCorrectCaptcha) { + $this->logCaptchaAttempt($captchaModel); + throw new LocalizedException(__('Incorrect CAPTCHA')); + } + } + + $this->logCaptchaAttempt($captchaModel); + } + + /** + * Log captcha attempts + * + * @param DefaultModel $captchaModel + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function logCaptchaAttempt(DefaultModel $captchaModel): void + { + $email = ''; + + if ($this->currentUser->getUserType() == UserContextInterface::USER_TYPE_CUSTOMER) { + $email = $this->customerRepository->getById($this->currentUser->getUserId())->getEmail(); + } + + $captchaModel->logAttempt($email); + } +} diff --git a/app/code/Magento/SendFriend/Model/SendFriend.php b/app/code/Magento/SendFriend/Model/SendFriend.php index c69d6342b4892..825cac2bae10b 100644 --- a/app/code/Magento/SendFriend/Model/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/SendFriend.php @@ -16,6 +16,7 @@ * @method \Magento\SendFriend\Model\SendFriend setTime(int $value) * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api @@ -162,6 +163,8 @@ protected function _construct() } /** + * Sends email to recipients + * * @return $this * @throws CoreException */ @@ -236,7 +239,7 @@ public function validate() } $email = $this->getSender()->getEmail(); - if (empty($email) or !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { + if (empty($email) || !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { $errors[] = __('Invalid Sender Email'); } @@ -281,13 +284,13 @@ public function setRecipients($recipients) // validate array if (!is_array( $recipients - ) or !isset( + ) || !isset( $recipients['email'] - ) or !isset( + ) || !isset( $recipients['name'] - ) or !is_array( + ) || !is_array( $recipients['email'] - ) or !is_array( + ) || !is_array( $recipients['name'] ) ) { @@ -487,7 +490,7 @@ protected function _sentCountByCookies($increment = false) $oldTimes = explode(',', $oldTimes); foreach ($oldTimes as $oldTime) { $periodTime = $time - $this->_sendfriendData->getPeriod(); - if (is_numeric($oldTime) and $oldTime >= $periodTime) { + if (is_numeric($oldTime) && $oldTime >= $periodTime) { $newTimes[] = $oldTime; } } diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php deleted file mode 100644 index 9d48133c1d500..0000000000000 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php +++ /dev/null @@ -1,423 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SendFriend\Test\Unit\Controller\Product; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SendTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\SendFriend\Controller\Product\Send */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorMock; - - /** @var \Magento\SendFriend\Model\SendFriend|\PHPUnit_Framework_MockObject_MockObject */ - protected $sendFriendMock; - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepositoryMock; - - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogSessionMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - protected function setUp() - { - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->getMockForAbstractClass(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sendFriendMock = $this->getMockBuilder(\Magento\SendFriend\Model\SendFriend::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->catalogSessionMock = $this->getMockBuilder(\Magento\Catalog\Model\Session::class) - ->setMethods(['getSendfriendFormData', 'setSendfriendFormData']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->getMock(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\SendFriend\Controller\Product\Send::class, - [ - 'request' => $this->requestMock, - 'coreRegistry' => $this->registryMock, - 'formKeyValidator' => $this->validatorMock, - 'sendFriend' => $this->sendFriendMock, - 'productRepository' => $this->productRepositoryMock, - 'catalogSession' => $this->catalogSessionMock, - 'messageManager' => $this->messageManagerMock, - 'resultFactory' => $this->resultFactoryMock, - 'eventManager' => $this->eventManagerMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $productId = 11; - $formData = ['some' => 'data']; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->with('product', $productMock, false); - - $this->sendFriendMock->expects($this->once()) - ->method('getMaxSendsToFriend') - ->willReturn(11); - $this->sendFriendMock->expects($this->once()) - ->method('isExceedLimit') - ->willReturn(false); - - $this->messageManagerMock->expects($this->never()) - ->method('addNotice'); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE, []) - ->willReturn($pageMock); - - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('sendfriend_product', ['product' => $productMock]); - - $this->catalogSessionMock->expects($this->once()) - ->method('getSendfriendFormData') - ->willReturn($formData); - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with(true); - - /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) - ->disableOriginalConstructor() - ->getMock(); - - $pageMock->expects($this->once()) - ->method('getLayout') - ->willReturn($layoutMock); - - /** @var \Magento\SendFriend\Block\Send|\PHPUnit_Framework_MockObject_MockObject $blockMock */ - $blockMock = $this->getMockBuilder(\Magento\SendFriend\Block\Send::class) - ->disableOriginalConstructor() - ->getMock(); - - $layoutMock->expects($this->once()) - ->method('getBlock') - ->with('sendfriend.send') - ->willReturn($blockMock); - - $blockMock->expects($this->once()) - ->method('setFormData') - ->with($formData) - ->willReturnSelf(); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutBlock() - { - $productId = 11; - $formData = ['some' => 'data']; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->with('product', $productMock, false); - - $this->sendFriendMock->expects($this->once()) - ->method('getMaxSendsToFriend') - ->willReturn(11); - $this->sendFriendMock->expects($this->once()) - ->method('isExceedLimit') - ->willReturn(false); - - $this->messageManagerMock->expects($this->never()) - ->method('addNotice'); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE, []) - ->willReturn($pageMock); - - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('sendfriend_product', ['product' => $productMock]); - - $this->catalogSessionMock->expects($this->once()) - ->method('getSendfriendFormData') - ->willReturn($formData); - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with(true); - - /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) - ->disableOriginalConstructor() - ->getMock(); - - $pageMock->expects($this->once()) - ->method('getLayout') - ->willReturn($layoutMock); - - $layoutMock->expects($this->once()) - ->method('getBlock') - ->with('sendfriend.send') - ->willReturn(false); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - public function testExecuteWithNoticeAndNoData() - { - $productId = 11; - $formData = null; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->with('product', $productMock, false); - - $this->sendFriendMock->expects($this->exactly(2)) - ->method('getMaxSendsToFriend') - ->willReturn(11); - $this->sendFriendMock->expects($this->once()) - ->method('isExceedLimit') - ->willReturn(true); - - $this->messageManagerMock->expects($this->once()) - ->method('addNotice') - ->with(__('You can\'t send messages more than %1 times an hour.', 11)) - ->willReturnSelf(); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE, []) - ->willReturn($pageMock); - - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('sendfriend_product', ['product' => $productMock]); - - $this->catalogSessionMock->expects($this->once()) - ->method('getSendfriendFormData') - ->willReturn($formData); - $this->catalogSessionMock->expects($this->never()) - ->method('setSendfriendFormData'); - - $pageMock->expects($this->never()) - ->method('getLayout'); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - public function testExecuteWithoutParam() - { - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn(null); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, []) - ->willReturn($forwardMock); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithoutProduct() - { - $productId = 11; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Product Exception.'))); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, []) - ->willReturn($forwardMock); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithNonVisibleProduct() - { - $productId = 11; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(false); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, []) - ->willReturn($forwardMock); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php deleted file mode 100644 index c7881f366f520..0000000000000 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php +++ /dev/null @@ -1,906 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SendFriend\Test\Unit\Controller\Product; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SendmailTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\SendFriend\Controller\Product\Sendmail */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorMock; - - /** @var \Magento\SendFriend\Model\SendFriend|\PHPUnit_Framework_MockObject_MockObject */ - protected $sendFriendMock; - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepositoryMock; - - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepositoryMock; - - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogSessionMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectMock; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlBuilderMock; - - protected function setUp() - { - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getPost', 'getPostValue', 'getParam']) - ->getMockForAbstractClass(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sendFriendMock = $this->getMockBuilder(\Magento\SendFriend\Model\SendFriend::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->categoryRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\CategoryRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->catalogSessionMock = $this->getMockBuilder(\Magento\Catalog\Model\Session::class) - ->setMethods(['getSendfriendFormData', 'setSendfriendFormData']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->getMock(); - $this->redirectMock = $this->getMockBuilder(\Magento\Framework\App\Response\RedirectInterface::class) - ->getMock(); - $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) - ->getMock(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\SendFriend\Controller\Product\Sendmail::class, - [ - 'request' => $this->requestMock, - 'coreRegistry' => $this->registryMock, - 'formKeyValidator' => $this->validatorMock, - 'sendFriend' => $this->sendFriendMock, - 'productRepository' => $this->productRepositoryMock, - 'categoryRepository' => $this->categoryRepositoryMock, - 'catalogSession' => $this->catalogSessionMock, - 'messageManager' => $this->messageManagerMock, - 'resultFactory' => $this->resultFactoryMock, - 'eventManager' => $this->eventManagerMock, - 'redirect' => $this->redirectMock, - 'url' => $this->urlBuilderMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $productUrl = 'product_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - /** @var \Magento\Catalog\Api\Data\CategoryInterface|\PHPUnit_Framework_MockObject_MockObject $categoryMock */ - $categoryMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\CategoryInterface::class) - ->getMockForAbstractClass(); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willReturn($categoryMock); - - $productMock->expects($this->once()) - ->method('setCategory') - ->with($categoryMock); - - $this->registryMock->expects($this->exactly(2)) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ['current_category', $categoryMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(true); - $this->sendFriendMock->expects($this->once()) - ->method('send') - ->willReturnSelf(); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('The link to a friend was sent.')) - ->willReturnSelf(); - - $productMock->expects($this->once()) - ->method('getProductUrl') - ->willReturn($productUrl); - - $this->redirectMock->expects($this->once()) - ->method('success') - ->with($productUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($productUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategory() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(['Some error']); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Some error')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategoryWithProblems() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn('Some error'); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('We found some problems with the data.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithLocalizedException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('Localized Exception.'))); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Localized Exception.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $exception = new \Exception(__('Exception.')); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException($exception); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addException') - ->with($exception, __('Some emails were not sent.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutProduct() - { - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutData() - { - $productId = 11; - $formData = ''; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithoutFormKey() - { - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(false); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index 30aecf13c3588..932093004cf7a 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -9,7 +9,9 @@ "magento/framework": "*", "magento/module-catalog": "*", "magento/module-customer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-captcha": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SendFriend/etc/config.xml b/app/code/Magento/SendFriend/etc/config.xml index 9fa005dcd2fd4..d65e5a4a073dd 100644 --- a/app/code/Magento/SendFriend/etc/config.xml +++ b/app/code/Magento/SendFriend/etc/config.xml @@ -17,5 +17,21 @@ <check_by>0</check_by> </email> </sendfriend> + <captcha translate="label"> + <frontend> + <areas> + <product_sendtofriend_form> + <label>Send To Friend Form</label> + </product_sendtofriend_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <product_sendtofriend_form>1</product_sendtofriend_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/SendFriend/etc/module.xml b/app/code/Magento/SendFriend/etc/module.xml index 01c267b3c4fcb..7876ef88618c2 100644 --- a/app/code/Magento/SendFriend/etc/module.xml +++ b/app/code/Magento/SendFriend/etc/module.xml @@ -10,6 +10,7 @@ <module name="Magento_SendFriend" > <sequence> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml index 8065b7e236132..4d6f3d8c628b2 100644 --- a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml +++ b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml @@ -13,7 +13,7 @@ </action> </referenceBlock> <referenceContainer name="content"> - <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" template="Magento_SendFriend::send.phtml"> + <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" cacheable="false" template="Magento_SendFriend::send.phtml"> <container name="form.additional.info" as="form_additional_info"/> </block> </referenceContainer> diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index 4922a9f365ced..3e00353a9157d 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -108,6 +108,7 @@ </div> <?= $block->getChildHtml('form_additional_info') ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" diff --git a/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php new file mode 100644 index 0000000000000..ec1ee277a5a51 --- /dev/null +++ b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Shipping\Block\DataProviders\Tracking; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Shipping\Model\Tracking\Result\Status; + +/** + * Extension point to provide ability to change tracking details titles + */ +class DeliveryDateTitle implements ArgumentInterface +{ + /** + * Returns Title in case if carrier defined + * + * @param Status $trackingStatus + * @return \Magento\Framework\Phrase|string + */ + public function getTitle(Status $trackingStatus) + { + return $trackingStatus->getCarrier() ? __('Delivered on:') : ''; + } +} diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php index fa83db490e380..c417a202321b2 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php @@ -1,14 +1,27 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; -class AddTrack extends \Magento\Backend\App\Action +/** + * Add new tracking number to shipment controller. + */ +class AddTrack extends Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -18,27 +31,54 @@ class AddTrack extends \Magento\Backend\App\Action const ADMIN_RESOURCE = 'Magento_Sales::shipment'; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader + * @var ShipmentLoader */ protected $shipmentLoader; + /** + * @var ShipmentRepositoryInterface + */ + private $shipmentRepository; + + /** + * @var ShipmentTrackInterfaceFactory + */ + private $trackFactory; + + /** + * @var SerializerInterface + */ + private $serializer; + /** * @param Action\Context $context - * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader + * @param ShipmentLoader $shipmentLoader + * @param ShipmentRepositoryInterface|null $shipmentRepository + * @param ShipmentTrackInterfaceFactory|null $trackFactory + * @param SerializerInterface|null $serializer */ public function __construct( Action\Context $context, - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader + ShipmentLoader $shipmentLoader, + ShipmentRepositoryInterface $shipmentRepository = null, + ShipmentTrackInterfaceFactory $trackFactory = null, + SerializerInterface $serializer = null ) { - $this->shipmentLoader = $shipmentLoader; parent::__construct($context); + + $this->shipmentLoader = $shipmentLoader; + $this->shipmentRepository = $shipmentRepository ?: ObjectManager::getInstance() + ->get(ShipmentRepositoryInterface::class); + $this->trackFactory = $trackFactory ?: ObjectManager::getInstance() + ->get(ShipmentTrackInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(SerializerInterface::class); } /** - * Add new tracking number action + * Add new tracking number action. * - * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @return ResultInterface */ public function execute() { @@ -46,28 +86,29 @@ public function execute() $carrier = $this->getRequest()->getPost('carrier'); $number = $this->getRequest()->getPost('number'); $title = $this->getRequest()->getPost('title'); + if (empty($carrier)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please specify a carrier.')); + throw new LocalizedException(__('Please specify a carrier.')); } if (empty($number)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a tracking number.')); + throw new LocalizedException(__('Please enter a tracking number.')); } + $this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id')); $this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id')); $this->shipmentLoader->setShipment($this->getRequest()->getParam('shipment')); $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); $shipment = $this->shipmentLoader->load(); if ($shipment) { - $track = $this->_objectManager->create( - \Magento\Sales\Model\Order\Shipment\Track::class - )->setNumber( + $track = $this->trackFactory->create()->setNumber( $number )->setCarrierCode( $carrier )->setTitle( $title ); - $shipment->addTrack($track)->save(); + $shipment->addTrack($track); + $this->shipmentRepository->save($shipment); $this->_view->loadLayout(); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Shipments')); @@ -78,16 +119,18 @@ public function execute() 'message' => __('We can\'t initialize shipment for adding tracking number.'), ]; } - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $response = ['error' => true, 'message' => $e->getMessage()]; } catch (\Exception $e) { $response = ['error' => true, 'message' => __('Cannot add tracking number.')]; } - if (is_array($response)) { - $response = $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($response); - $this->getResponse()->representJson($response); - } else { - $this->getResponse()->setBody($response); + + if (\is_array($response)) { + $response = $this->serializer->serialize($response); + + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($response); } + + return $this->resultFactory->create(ResultFactory::TYPE_RAW)->setContents($response); } } 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 8bd64ccf82d88..100ba029beabd 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -1,13 +1,13 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\ResultFactory; use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; /** @@ -48,17 +48,22 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac * @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 */ 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\Email\Sender\ShipmentSender $shipmentSender, + \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface $shipmentValidator = null ) { + parent::__construct($context); + $this->shipmentLoader = $shipmentLoader; $this->labelGenerator = $labelGenerator; $this->shipmentSender = $shipmentSender; - parent::__construct($context); + $this->shipmentValidator = $shipmentValidator ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class); } /** @@ -84,9 +89,10 @@ protected function _saveShipment($shipment) /** * Save shipment + * * We can save only new shipment. Existing shipments are not editable * - * @return void + * @return \Magento\Framework\Controller\ResultInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -98,7 +104,7 @@ public function execute() $formKeyIsValid = $this->_formKeyValidator->validate($this->getRequest()); $isPost = $this->getRequest()->isPost(); if (!$formKeyIsValid || !$isPost) { - $this->messageManager->addError(__('We can\'t save the shipment right now.')); + $this->messageManager->addErrorMessage(__('We can\'t save the shipment right now.')); return $resultRedirect->setPath('sales/order/index'); } @@ -118,8 +124,7 @@ public function execute() $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); $shipment = $this->shipmentLoader->load(); if (!$shipment) { - $this->_forward('noroute'); - return; + return $this->resultFactory->create(ResultFactory::TYPE_FORWARD)->forward('noroute'); } if (!empty($data['comment_text'])) { @@ -132,15 +137,13 @@ public function execute() $shipment->setCustomerNote($data['comment_text']); $shipment->setCustomerNoteNotify(isset($data['comment_customer_notify'])); } - $validationResult = $this->getShipmentValidator() - ->validate($shipment, [QuantityValidator::class]); + $validationResult = $this->shipmentValidator->validate($shipment, [QuantityValidator::class]); if ($validationResult->hasMessages()) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); - $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); - return; + return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } $shipment->register(); @@ -160,7 +163,7 @@ public function execute() $shipmentCreatedMessage = __('The shipment has been created.'); $labelCreatedMessage = __('You created the shipping label.'); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( $isNeedCreateLabel ? $shipmentCreatedMessage . ' ' . $labelCreatedMessage : $shipmentCreatedMessage ); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); @@ -169,8 +172,8 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage($e->getMessage()); } else { - $this->messageManager->addError($e->getMessage()); - $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + $this->messageManager->addErrorMessage($e->getMessage()); + return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -178,29 +181,14 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage(__('An error occurred while creating shipping label.')); } else { - $this->messageManager->addError(__('Cannot save shipment.')); - $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + $this->messageManager->addErrorMessage(__('Cannot save shipment.')); + return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } if ($isNeedCreateLabel) { - $this->getResponse()->representJson($responseAjax->toJson()); - } else { - $this->_redirect('sales/order/view', ['order_id' => $shipment->getOrderId()]); - } - } - - /** - * @return \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface - * @deprecated 100.1.1 - */ - private function getShipmentValidator() - { - if ($this->shipmentValidator === null) { - $this->shipmentValidator = $this->_objectManager->get( - \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class - ); + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($responseAjax->toJson()); } - return $this->shipmentValidator; + return $resultRedirect->setPath('sales/order/view', ['order_id' => $shipment->getOrderId()]); } } diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index f9030ee75630b..76555ce8a6d8c 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -160,7 +160,8 @@ public function getConfigFlag($field) } /** - * Do request to shipment + * Do request to shipment. + * * Implementation must be in overridden method * * @param Request $request @@ -173,7 +174,8 @@ public function requestToShipment($request) } /** - * Do return of shipment + * Do return of shipment. + * * Implementation must be in overridden method * * @param Request $request @@ -275,6 +277,8 @@ public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $para } /** + * Validate request for available ship countries. + * * @param \Magento\Framework\DataObject $request * @return $this|bool|false|\Magento\Framework\Model\AbstractModel * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -400,6 +404,8 @@ public function getSortOrder() } /** + * Allows free shipping when all product items have free shipping. + * * @param \Magento\Quote\Model\Quote\Address\RateRequest $request * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -531,10 +537,10 @@ protected function _getPerorderPrice($cost, $handlingType, $handlingFee) } /** - * Sets the number of boxes for shipping + * Gets the average weight of each box available for shipping * - * @param int $weight in some measure - * @return int + * @param float $weight in some measure + * @return float */ public function getTotalNumOfBoxes($weight) { @@ -545,7 +551,7 @@ public function getTotalNumOfBoxes($weight) $maxPackageWeight = $this->getConfigData('max_package_weight'); if ($weight > $maxPackageWeight && $maxPackageWeight != 0) { $this->_numBoxes = ceil($weight / $maxPackageWeight); - $weight = $weight / $this->_numBoxes; + $weight = (float)$weight / $this->_numBoxes; } return $weight; @@ -671,7 +677,8 @@ protected function filterDebugData($data) } /** - * Recursive replace sensitive xml nodes values by specified mask + * Recursive replace sensitive xml nodes values by specified mask. + * * @param \SimpleXMLElement $xml * @return void */ diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml index 512ef8389bb7b..d700aa622c177 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml @@ -13,6 +13,13 @@ <entity name="freeActiveEnable" type="active"> <data key="value">1</data> </entity> + <!-- Disable Free Shipping method --> + <entity name="FreeShippingMethodDisableConfig" type="free_shipping_method"> + <requiredEntity type="active">freeActiveDisable</requiredEntity> + </entity> + <entity name="freeActiveDisable" type="active"> + <data key="value">0</data> + </entity> <!-- Free Shipping method default setup --> <entity name="FreeShippinMethodDefault" type="free_shipping_method"> <requiredEntity type="active">freeActiveDefault</requiredEntity> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml index a7bf82588f7c7..0345c3f2949f4 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -15,5 +15,6 @@ <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-qty input.qty-item" parameterized="true"/> <element name="nameColumn" type="text" selector=".order-shipment-table .col-product .product-title"/> <element name="skuColumn" type="text" selector=".order-shipment-table .col-product .product-sku-block"/> + <element name="itemQtyInvoiced" type="text" selector="(//*[@class='col-ordered-qty']//th[contains(text(), 'Invoiced')]/following-sibling::td)[{{var}}]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php index cf3bf68e10416..1c9cdb1fa7d5b 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php @@ -6,127 +6,162 @@ namespace Magento\Shipping\Test\Unit\Controller\Adminhtml\Order\Shipment; -use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\ViewInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Page\Config; +use Magento\Framework\View\Page\Title; +use Magento\Framework\View\Result\Page; +use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; /** - * Class AddTrackTest + * Class AddTrackTest covers AddTrack controller. * - * @package Magento\Shipping\Controller\Adminhtml\Order\Shipment * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AddTrackTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader|\PHPUnit_Framework_MockObject_MockObject + * @var ShipmentLoader|\PHPUnit_Framework_MockObject_MockObject */ - protected $shipmentLoader; + private $shipmentLoader; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack + * @var AddTrack */ - protected $controller; + private $controller; /** - * @var Action\Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; + private $context; /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; + private $request; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; + private $response; /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject + * @var ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManager; + private $view; /** - * @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Page|\PHPUnit_Framework_MockObject_MockObject */ - protected $view; + private $resultPageMock; /** - * @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject + * @var Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultPageMock; + private $pageConfigMock; /** - * @var \Magento\Framework\View\Page\Config|\PHPUnit_Framework_MockObject_MockObject + * @var Title|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageConfigMock; + private $pageTitleMock; /** - * @var \Magento\Framework\View\Page\Title|\PHPUnit_Framework_MockObject_MockObject + * @var ShipmentTrackInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageTitleMock; + private $trackFactory; + /** + * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $rawResult; + + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->shipmentLoader = $this->getMockBuilder( - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class + ShipmentLoader::class ) ->disableOriginalConstructor() ->setMethods(['setShipmentId', 'setOrderId', 'setShipment', 'setTracking', 'load']) ->getMock(); $this->context = $this->createPartialMock( - \Magento\Backend\App\Action\Context::class, + Context::class, [ 'getRequest', 'getResponse', 'getRedirect', 'getObjectManager', 'getTitle', - 'getView' + 'getView', + 'getResultFactory' ] ); $this->response = $this->createPartialMock( - \Magento\Framework\App\ResponseInterface::class, + ResponseInterface::class, ['setRedirect', 'sendResponse', 'setBody'] ); - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + $this->request = $this->getMockBuilder(Http::class) ->disableOriginalConstructor()->getMock(); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); - $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) + $this->view = $this->createMock(ViewInterface::class); + $this->resultPageMock = $this->getMockBuilder(Page::class) ->disableOriginalConstructor() ->getMock(); - $this->pageConfigMock = $this->getMockBuilder(\Magento\Framework\View\Page\Config::class) + $this->pageConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->pageTitleMock = $this->getMockBuilder(\Magento\Framework\View\Page\Title::class) + $this->pageTitleMock = $this->getMockBuilder(Title::class) ->disableOriginalConstructor() ->getMock(); + $this->trackFactory = $this->getMockBuilder(ShipmentTrackInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); + $this->rawResult = $this->getMockBuilder(ResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setContents']) + ->getMockForAbstractClass(); + $resultFactory = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); $this->context->expects($this->once()) ->method('getRequest') ->will($this->returnValue($this->request)); $this->context->expects($this->once()) ->method('getResponse') ->will($this->returnValue($this->response)); - $this->context->expects($this->once()) - ->method('getObjectManager') - ->will($this->returnValue($this->objectManager)); $this->context->expects($this->once()) ->method('getView') ->will($this->returnValue($this->view)); + $resultFactory->expects($this->once()) + ->method('create') + ->willReturn($this->rawResult); + $this->context->expects($this->once()) + ->method('getResultFactory') + ->willReturn($resultFactory); $this->controller = $objectManagerHelper->getObject( - \Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack::class, + AddTrack::class, [ 'context' => $this->context, 'shipmentLoader' => $this->shipmentLoader, 'request' => $this->request, 'response' => $this->response, - 'view' => $this->view + 'view' => $this->view, + 'trackFactory' => $this->trackFactory, ] ); } @@ -144,7 +179,7 @@ public function testExecute() $tracking = []; $shipmentData = ['items' => [], 'send_email' => '']; $shipment = $this->createPartialMock( - \Magento\Sales\Model\Order\Shipment::class, + Shipment::class, ['addTrack', '__wakeup', 'save'] ); $this->request->expects($this->any()) @@ -152,8 +187,10 @@ public function testExecute() ->will( $this->returnValueMap( [ - ['order_id', null, $orderId], ['shipment_id', null, $shipmentId], - ['shipment', null, $shipmentData], ['tracking', null, $tracking], + ['order_id', null, $orderId], + ['shipment_id', null, $shipmentId], + ['shipment', null, $shipmentData], + ['tracking', null, $tracking], ] ) ); @@ -183,14 +220,13 @@ public function testExecute() $this->shipmentLoader->expects($this->once()) ->method('load') ->will($this->returnValue($shipment)); - $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + $track = $this->getMockBuilder(Track::class) ->disableOriginalConstructor() ->setMethods(['__wakeup', 'setNumber', 'setCarrierCode', 'setTitle']) ->getMock(); - $this->objectManager->expects($this->atLeastOnce()) + $this->trackFactory->expects($this->once()) ->method('create') - ->with(\Magento\Sales\Model\Order\Shipment\Track::class) - ->will($this->returnValue($track)); + ->willReturn($track); $track->expects($this->once()) ->method('setNumber') ->with($number) @@ -206,8 +242,8 @@ public function testExecute() $this->view->expects($this->once()) ->method('loadLayout') ->will($this->returnSelf()); - $layout = $this->createMock(\Magento\Framework\View\LayoutInterface::class); - $menuBlock = $this->createPartialMock(\Magento\Framework\View\Element\BlockInterface::class, ['toHtml']); + $layout = $this->createMock(LayoutInterface::class); + $menuBlock = $this->createPartialMock(BlockInterface::class, ['toHtml']); $html = 'html string'; $this->view->expects($this->once()) ->method('getLayout') @@ -235,9 +271,10 @@ public function testExecute() $this->pageConfigMock->expects($this->any()) ->method('getTitle') ->willReturn($this->pageTitleMock); - $this->response->expects($this->once()) - ->method('setBody') - ->with($html); - $this->assertNull($this->controller->execute()); + $this->rawResult->expects($this->once()) + ->method('setContents') + ->with($html) + ->willReturnSelf(); + $this->assertInstanceOf(ResultInterface::class, $this->controller->execute()); } } 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 f841728416f82..c253900501d18 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 @@ -142,7 +142,7 @@ protected function setUp() ); $this->messageManager = $this->createPartialMock( \Magento\Framework\Message\Manager::class, - ['addSuccess', 'addError'] + ['addSuccessMessage', 'addErrorMessage'] ); $this->session = $this->createPartialMock( \Magento\Backend\Model\Session::class, @@ -236,7 +236,7 @@ public function testExecute($formKeyIsValid, $isPost) if (!$formKeyIsValid || !$isPost) { $this->messageManager->expects($this->once()) - ->method('addError'); + ->method('addErrorMessage'); $this->resultRedirect->expects($this->once()) ->method('setPath') @@ -325,12 +325,11 @@ public function testExecute($formKeyIsValid, $isPost) ->method('get') ->with(\Magento\Backend\Model\Session::class) ->will($this->returnValue($this->session)); - $path = 'sales/order/view'; $arguments = ['order_id' => $orderId]; $shipment->expects($this->once()) ->method('getOrderId') ->will($this->returnValue($orderId)); - $this->prepareRedirect($path, $arguments); + $this->prepareRedirect($arguments); $this->shipmentValidatorMock->expects($this->once()) ->method('validate') @@ -360,10 +359,9 @@ public function executeDataProvider() } /** - * @param string $path * @param array $arguments */ - protected function prepareRedirect($path, array $arguments = []) + protected function prepareRedirect(array $arguments = []) { $this->actionFlag->expects($this->any()) ->method('get') @@ -372,14 +370,8 @@ protected function prepareRedirect($path, array $arguments = []) $this->session->expects($this->any()) ->method('setIsUrlNotice') ->with(true); - - $url = $path . '/' . (!empty($arguments) ? $arguments['order_id'] : ''); - $this->helper->expects($this->atLeastOnce()) - ->method('getUrl') - ->with($path, $arguments) - ->will($this->returnValue($url)); - $this->response->expects($this->atLeastOnce()) - ->method('setRedirect') - ->with($url); + $this->resultRedirect->expects($this->once()) + ->method('setPath') + ->with('sales/order/view', $arguments); } } diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index c32b63bddab56..db0739d127b2b 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -61,7 +61,7 @@ <?php else: ?> class="admin__control-select" <?php endif; ?>> - <?php foreach ($block->getContainers() as $key => $value): ?> + <?php foreach ($containers as $key => $value): ?> <option value="<?= /* @escapeNotVerified */ $key ?>" > <?= /* @escapeNotVerified */ $value ?> </option> diff --git a/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml b/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml index 1f5b0ae4630ad..67d03da2599bf 100644 --- a/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml +++ b/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml @@ -8,7 +8,11 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="empty" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Shipping\Block\Tracking\Popup" name="shipping.tracking.popup" template="Magento_Shipping::tracking/popup.phtml" cacheable="false" /> + <block class="Magento\Shipping\Block\Tracking\Popup" name="shipping.tracking.popup" template="Magento_Shipping::tracking/popup.phtml" cacheable="false"> + <arguments> + <argument name="delivery_date_title" xsi:type="object">Magento\Shipping\Block\DataProviders\Tracking\DeliveryDateTitle</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml index 9253b47f82f5d..e8584d8f6ad51 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml @@ -77,7 +77,7 @@ $number = is_object($track) ? $track->getTracking() : $track['number']; <?php if ($track->getDeliverydate()): ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__('Delivered on:')) ?></th> + <th class="col label" scope="row"><?= $block->escapeHtml($parentBlock->getDeliveryDateTitle()->getTitle($track)) ?></th> <td class="col value"> <?= /* @noEscape */ $parentBlock->formatDeliveryDateTime($track->getDeliverydate(), $track->getDeliverytime()) ?> </td> diff --git a/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php b/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php index 5e90cf6e12f6e..91b6a8446894b 100644 --- a/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php +++ b/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php @@ -48,6 +48,8 @@ protected function _construct() } /** + * Configure form for sitemap. + * * @return $this */ protected function _prepareForm() @@ -73,7 +75,8 @@ protected function _prepareForm() 'name' => 'sitemap_filename', 'required' => true, 'note' => __('example: sitemap.xml'), - 'value' => $model->getSitemapFilename() + 'value' => $model->getSitemapFilename(), + 'class' => 'validate-length maximum-length-32' ] ); diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index 29d50ea8408fd..1c807cbfc194e 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -5,10 +5,14 @@ */ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -class Delete extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +/** + * Controller class Delete. Represents adminhtml request flow for a sitemap deletion + */ +class Delete extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpPostActionInterface { /** * @var \Magento\Framework\Filesystem diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php index 111353550b9cd..14771e7f03a3b 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; -class Edit extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Controller class Edit. Responsible for rendering of a sitemap edit page + */ +class Edit extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpGetActionInterface { /** * Core registry diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php index 9592ab6f57c55..8eeeb5bf6bc12 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php @@ -1,16 +1,21 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Sitemap\Controller\Adminhtml\Sitemap; use Magento\Store\Model\App\Emulation; use Magento\Framework\App\ObjectManager; -class Generate extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +/** + * Generate sitemap file + */ +class Generate extends Sitemap implements HttpGetActionInterface { /** @var \Magento\Store\Model\App\Emulation $appEmulation */ private $appEmulation; @@ -44,14 +49,7 @@ public function execute() // if sitemap record exists if ($sitemap->getId()) { try { - //We need to emulate to get the correct frontend URL for the product images - $this->appEmulation->startEnvironmentEmulation( - $sitemap->getStoreId(), - \Magento\Framework\App\Area::AREA_FRONTEND, - true - ); $sitemap->generateXml(); - $this->messageManager->addSuccessMessage( __('The sitemap "%1" has been generated.', $sitemap->getSitemapFilename()) ); @@ -59,8 +57,6 @@ public function execute() $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addExceptionMessage($e, __('We can\'t generate the sitemap right now.')); - } finally { - $this->appEmulation->stopEnvironmentEmulation(); } } else { $this->messageManager->addErrorMessage(__('We can\'t find a sitemap to generate.')); diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php index 1e0d1cb248f00..5230de0429778 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php @@ -5,12 +5,74 @@ */ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; -use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Controller; +use Magento\Framework\Validator\StringLength; +use Magento\MediaStorage\Model\File\Validator\AvailablePath; +use Magento\Sitemap\Model\SitemapFactory; -class Save extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +/** + * Save sitemap controller. + */ +class Save extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpPostActionInterface { + /** + * Maximum length of sitemap filename + */ + const MAX_FILENAME_LENGTH = 32; + + /** + * @var StringLength + */ + private $stringValidator; + + /** + * @var AvailablePath + */ + private $pathValidator; + + /** + * @var \Magento\Sitemap\Helper\Data + */ + private $sitemapHelper; + + /** + * @var \Magento\Framework\Filesystem + */ + private $filesystem; + + /** + * @var SitemapFactory + */ + private $sitemapFactory; + + /** + * Save constructor. + * @param Context $context + * @param StringLength $stringValidator + * @param AvailablePath $pathValidator + * @param \Magento\Sitemap\Helper\Data $sitemapHelper + * @param \Magento\Framework\Filesystem $filesystem + * @param SitemapFactory $sitemapFactory + */ + public function __construct( + Context $context, + StringLength $stringValidator = null, + AvailablePath $pathValidator = null, + \Magento\Sitemap\Helper\Data $sitemapHelper = null, + \Magento\Framework\Filesystem $filesystem = null, + SitemapFactory $sitemapFactory = null + ) { + parent::__construct($context); + $this->stringValidator = $stringValidator ?: $this->_objectManager->get(StringLength::class); + $this->pathValidator = $pathValidator ?: $this->_objectManager->get(AvailablePath::class); + $this->sitemapHelper = $sitemapHelper ?: $this->_objectManager->get(\Magento\Sitemap\Helper\Data::class); + $this->filesystem = $filesystem ?: $this->_objectManager->get(\Magento\Framework\Filesystem::class); + $this->sitemapFactory = $sitemapFactory ?: $this->_objectManager->get(SitemapFactory::class); + } + /** * Validate path for generation * @@ -23,17 +85,25 @@ protected function validatePath(array $data) if (!empty($data['sitemap_filename']) && !empty($data['sitemap_path'])) { $data['sitemap_path'] = '/' . ltrim($data['sitemap_path'], '/'); $path = rtrim($data['sitemap_path'], '\\/') . '/' . $data['sitemap_filename']; - /** @var $validator \Magento\MediaStorage\Model\File\Validator\AvailablePath */ - $validator = $this->_objectManager->create(\Magento\MediaStorage\Model\File\Validator\AvailablePath::class); - /** @var $helper \Magento\Sitemap\Helper\Data */ - $helper = $this->_objectManager->get(\Magento\Sitemap\Helper\Data::class); - $validator->setPaths($helper->getValidPaths()); - if (!$validator->isValid($path)) { - foreach ($validator->getMessages() as $message) { + $this->pathValidator->setPaths($this->sitemapHelper->getValidPaths()); + if (!$this->pathValidator->isValid($path)) { + foreach ($this->pathValidator->getMessages() as $message) { + $this->messageManager->addErrorMessage($message); + } + // save data in session + $this->_session->setFormData($data); + // redirect to edit form + return false; + } + + $filename = rtrim($data['sitemap_filename']); + $this->stringValidator->setMax(self::MAX_FILENAME_LENGTH); + if (!$this->stringValidator->isValid($filename)) { + foreach ($this->stringValidator->getMessages() as $message) { $this->messageManager->addErrorMessage($message); } // save data in session - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setFormData($data); + $this->_session->setFormData($data); // redirect to edit form return false; } @@ -49,9 +119,8 @@ protected function validatePath(array $data) */ protected function clearSiteMap(\Magento\Sitemap\Model\Sitemap $model) { - /** @var \Magento\Framework\Filesystem\Directory\Write $directory */ - $directory = $this->_objectManager->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::ROOT); + /** @var \Magento\Framework\Filesystem $directory */ + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::ROOT); if ($this->getRequest()->getParam('sitemap_id')) { $model->load($this->getRequest()->getParam('sitemap_id')); @@ -74,7 +143,7 @@ protected function saveData($data) { // init model and set data /** @var \Magento\Sitemap\Model\Sitemap $model */ - $model = $this->_objectManager->create(\Magento\Sitemap\Model\Sitemap::class); + $model = $this->sitemapFactory->create(); $this->clearSiteMap($model); $model->setData($data); @@ -85,13 +154,13 @@ protected function saveData($data) // display success message $this->messageManager->addSuccessMessage(__('You saved the sitemap.')); // clear previously saved data from session - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setFormData(false); + $this->_session->setFormData(false); return $model->getId(); } catch (\Exception $e) { // display error message $this->messageManager->addErrorMessage($e->getMessage()); // save data in session - $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setFormData($data); + $this->_session->setFormData($data); } return false; } diff --git a/app/code/Magento/Sitemap/Model/EmailNotification.php b/app/code/Magento/Sitemap/Model/EmailNotification.php new file mode 100644 index 0000000000000..27c042870a1d6 --- /dev/null +++ b/app/code/Magento/Sitemap/Model/EmailNotification.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Sitemap\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Translate\Inline\StateInterface; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Store\Model\ScopeInterface; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Sitemap\Model\Observer as Observer; +use Psr\Log\LoggerInterface; + +/** + * Sends emails for the scheduled generation of the sitemap file + */ +class EmailNotification +{ + /** + * @var \Magento\Framework\Translate\Inline\StateInterface + */ + private $inlineTranslation; + + /** + * Core store config + * + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var \Magento\Framework\Mail\Template\TransportBuilder + */ + private $transportBuilder; + + /** + * @var \Psr\Log\LoggerInterface $logger + */ + private $logger; + + /** + * EmailNotification constructor. + * @param StateInterface $inlineTranslation + * @param TransportBuilder $transportBuilder + * @param ScopeConfigInterface $scopeConfig + * @param LoggerInterface $logger + */ + public function __construct( + StateInterface $inlineTranslation, + TransportBuilder $transportBuilder, + ScopeConfigInterface $scopeConfig, + LoggerInterface $logger + ) { + $this->inlineTranslation = $inlineTranslation; + $this->scopeConfig = $scopeConfig; + $this->transportBuilder = $transportBuilder; + $this->logger = $logger; + } + + /** + * Send's error email if sitemap generated with errors. + * + * @param array| $errors + */ + public function sendErrors($errors) + { + $this->inlineTranslation->suspend(); + try { + $this->transportBuilder->setTemplateIdentifier( + $this->scopeConfig->getValue( + Observer::XML_PATH_ERROR_TEMPLATE, + ScopeInterface::SCOPE_STORE + ) + )->setTemplateOptions( + [ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + ] + )->setTemplateVars( + ['warnings' => join("\n", $errors)] + )->setFrom( + $this->scopeConfig->getValue( + Observer::XML_PATH_ERROR_IDENTITY, + ScopeInterface::SCOPE_STORE + ) + )->addTo( + $this->scopeConfig->getValue( + Observer::XML_PATH_ERROR_RECIPIENT, + ScopeInterface::SCOPE_STORE + ) + ); + + $transport = $this->transportBuilder->getTransport(); + $transport->sendMessage(); + } catch (\Exception $e) { + $this->logger->error('Sitemap sendErrors: '.$e->getMessage()); + } finally { + $this->inlineTranslation->resume(); + } + } +} diff --git a/app/code/Magento/Sitemap/Model/Observer.php b/app/code/Magento/Sitemap/Model/Observer.php index a536ec998b827..ce74d738c4bc3 100644 --- a/app/code/Magento/Sitemap/Model/Observer.php +++ b/app/code/Magento/Sitemap/Model/Observer.php @@ -5,6 +5,11 @@ */ namespace Magento\Sitemap\Model; +use Magento\Sitemap\Model\EmailNotification as SitemapEmail; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Store\Model\ScopeInterface; + /** * Sitemap module observer * @@ -44,47 +49,32 @@ class Observer * * @var \Magento\Framework\App\Config\ScopeConfigInterface */ - protected $_scopeConfig; + private $scopeConfig; /** * @var \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory */ - protected $_collectionFactory; - - /** - * @var \Magento\Framework\Mail\Template\TransportBuilder - */ - protected $_transportBuilder; - - /** - * @var \Magento\Store\Model\StoreManagerInterface - */ - protected $_storeManager; + private $collectionFactory; /** - * @var \Magento\Framework\Translate\Inline\StateInterface + * @var $emailNotification */ - protected $inlineTranslation; + private $emailNotification; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory $collectionFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder - * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + * Observer constructor. + * @param ScopeConfigInterface $scopeConfig + * @param CollectionFactory $collectionFactory + * @param EmailNotification $emailNotification */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory $collectionFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, - \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + ScopeConfigInterface $scopeConfig, + CollectionFactory $collectionFactory, + SitemapEmail $emailNotification ) { - $this->_scopeConfig = $scopeConfig; - $this->_collectionFactory = $collectionFactory; - $this->_storeManager = $storeManager; - $this->_transportBuilder = $transportBuilder; - $this->inlineTranslation = $inlineTranslation; + $this->scopeConfig = $scopeConfig; + $this->collectionFactory = $collectionFactory; + $this->emailNotification = $emailNotification; } /** @@ -97,17 +87,20 @@ public function __construct( public function scheduledGenerateSitemaps() { $errors = []; - + $recipient = $this->scopeConfig->getValue( + Observer::XML_PATH_ERROR_RECIPIENT, + ScopeInterface::SCOPE_STORE + ); // check if scheduled generation enabled - if (!$this->_scopeConfig->isSetFlag( + if (!$this->scopeConfig->isSetFlag( self::XML_PATH_GENERATION_ENABLED, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ) ) { return; } - $collection = $this->_collectionFactory->create(); + $collection = $this->collectionFactory->create(); /* @var $collection \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection */ foreach ($collection as $sitemap) { /* @var $sitemap \Magento\Sitemap\Model\Sitemap */ @@ -117,41 +110,8 @@ public function scheduledGenerateSitemaps() $errors[] = $e->getMessage(); } } - - if ($errors && $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_RECIPIENT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ) { - $this->inlineTranslation->suspend(); - - $this->_transportBuilder->setTemplateIdentifier( - $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_TEMPLATE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - )->setTemplateOptions( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ] - )->setTemplateVars( - ['warnings' => join("\n", $errors)] - )->setFrom( - $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_IDENTITY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - )->addTo( - $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_RECIPIENT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ); - $transport = $this->_transportBuilder->getTransport(); - $transport->sendMessage(); - - $this->inlineTranslation->resume(); + if ($errors && $recipient) { + $this->emailNotification->sendErrors($errors); } } } diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php index 82024b3b015e5..1419fa375a117 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php @@ -5,12 +5,12 @@ */ namespace Magento\Sitemap\Model\ResourceModel\Catalog; +use Magento\Catalog\Helper\Product as HelperProduct; use Magento\Catalog\Model\Product\Image\UrlBuilder; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Store\Model\Store; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; -use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Store\Model\Store; /** * Sitemap resource product collection model @@ -259,7 +259,7 @@ protected function _joinAttribute($storeId, $attributeCode, $column = null) // Add attribute value to result set if needed if (isset($column)) { $this->_select->columns([ - $column => $columnValue + $column => $columnValue ]); } } @@ -282,7 +282,7 @@ protected function _getAttribute($attributeCode) 'attribute_id' => $attribute->getId(), 'table' => $attribute->getBackend()->getTable(), 'is_global' => $attribute->getIsGlobal() == - \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, 'backend_type' => $attribute->getBackendType(), ]; } @@ -324,7 +324,8 @@ public function getCollection($storeId) [] )->joinLeft( ['url_rewrite' => $this->getTable('url_rewrite')], - 'e.entity_id = url_rewrite.entity_id AND url_rewrite.is_autogenerated = 1 AND url_rewrite.metadata IS ' + 'e.entity_id = url_rewrite.entity_id AND url_rewrite.is_autogenerated = 1 ' + . 'AND NULLIF(url_rewrite.metadata,"") IS ' . $urlsConfigCondition . 'NULL' . $connection->quoteInto(' AND url_rewrite.store_id = ?', $store->getId()) . $connection->quoteInto(' AND url_rewrite.entity_type = ?', ProductUrlRewriteGenerator::ENTITY_TYPE), @@ -389,6 +390,7 @@ protected function _prepareProduct(array $productRow, $storeId) */ protected function _loadProductImages($product, $storeId) { + $this->_storeManager->setCurrentStore($storeId); /** @var $helper \Magento\Sitemap\Helper\Data */ $helper = $this->_sitemapData; $imageIncludePolicy = $helper->getProductImageIncludePolicy($storeId); diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index d58ff732c81d7..c35e20d997d85 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Model; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; @@ -15,6 +16,8 @@ use Magento\Sitemap\Model\ResourceModel\Sitemap as SitemapResource; /** + * Sitemap model. + * * @method string getSitemapType() * @method \Magento\Sitemap\Model\Sitemap setSitemapType(string $value) * @method string getSitemapFilename() @@ -154,12 +157,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento protected $dateTime; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [Value::CACHE_TAG]; /** * Item resolver diff --git a/app/code/Magento/Sitemap/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Sitemap/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..c8fd6a751cb22 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Data/AdminMenuData.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="AdminMenuSEOAndSearchSiteMap"> + <data key="pageTitle">Site Map</data> + <data key="title">Site Map</data> + <data key="dataUiId">magento-sitemap-catalog-sitemap</data> + </entity> +</entities> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml new file mode 100644 index 0000000000000..54543fab8649d --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingSiteMapNavigateMenuTest"> + <annotations> + <features value="Sitemap"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing site map navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Site Map"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14204"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSiteMapPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSEOAndSearchSiteMap.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSEOAndSearchSiteMap.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php index f77954101df7c..00f51b7e6c23f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php @@ -7,51 +7,83 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\Controller\ResultFactory; +use Magento\Sitemap\Controller\Adminhtml\Sitemap\Save; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Sitemap\Controller\Adminhtml\Sitemap\Save */ - protected $saveController; + private $saveController; /** * @var \Magento\Backend\App\Action\Context */ - protected $context; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - */ - protected $objectManagerHelper; + private $contextMock; /** * @var \Magento\Framework\HTTP\PhpEnvironment\Request|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; + private $requestMock; /** * @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; + private $resultFactoryMock; /** * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirectMock; + private $resultRedirectMock; /** * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; + private $messageManagerMock; + + /** + * @var \Magento\Framework\Validator\StringLength|\PHPUnit_Framework_MockObject_MockObject + */ + private $lengthValidator; + + /** + * @var \Magento\MediaStorage\Model\File\Validator\AvailablePath|\PHPUnit_Framework_MockObject_MockObject + */ + private $pathValidator; + + /** + * @var \Magento\Sitemap\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $helper; + + /** + * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileSystem; + + /** + * @var \Magento\Sitemap\Model\SitemapFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $siteMapFactory; + + /** + * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $session; protected function setUp() { + $this->contextMock = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) + ->disableOriginalConstructor() + ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() ->setMethods(['getPostValue']) @@ -66,27 +98,48 @@ protected function setUp() ->getMock(); $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) ->getMock(); - + $this->helper = $this->getMockBuilder(\Magento\Sitemap\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); $this->resultFactoryMock->expects($this->once()) ->method('create') ->with(ResultFactory::TYPE_REDIRECT) ->willReturn($this->resultRedirectMock); + $this->session = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->disableOriginalConstructor() + ->setMethods(['setFormData']) + ->getMock(); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->context = $this->objectManagerHelper->getObject( - \Magento\Backend\App\Action\Context::class, - [ - 'resultFactory' => $this->resultFactoryMock, - 'request' => $this->requestMock, - 'messageManager' => $this->messageManagerMock, - 'objectManager' => $this->objectManagerMock - ] - ); - $this->saveController = $this->objectManagerHelper->getObject( - \Magento\Sitemap\Controller\Adminhtml\Sitemap\Save::class, - [ - 'context' => $this->context - ] + $this->contextMock->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManagerMock); + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->contextMock->expects($this->once()) + ->method('getResultFactory') + ->willReturn($this->resultFactoryMock); + $this->contextMock->expects($this->once()) + ->method('getSession') + ->willReturn($this->session); + + $this->lengthValidator = $this->getMockBuilder(\Magento\Framework\Validator\StringLength::class) + ->disableOriginalConstructor() + ->getMock(); + $this->pathValidator = + $this->getMockBuilder(\Magento\MediaStorage\Model\File\Validator\AvailablePath::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fileSystem = $this->createMock(\Magento\Framework\Filesystem::class); + $this->siteMapFactory = $this->createMock(\Magento\Sitemap\Model\SitemapFactory::class); + + $this->saveController = new Save( + $this->contextMock, + $this->lengthValidator, + $this->pathValidator, + $this->helper, + $this->fileSystem, + $this->siteMapFactory ); } @@ -105,11 +158,8 @@ public function testSaveEmptyDataShouldRedirectToDefault() public function testTryToSaveInvalidDataShouldFailWithErrors() { - $validatorClass = \Magento\MediaStorage\Model\File\Validator\AvailablePath::class; - $helperClass = \Magento\Sitemap\Helper\Data::class; $validPaths = []; $messages = ['message1', 'message2']; - $sessionClass = \Magento\Backend\Model\Session::class; $data = ['sitemap_filename' => 'sitemap_filename', 'sitemap_path' => '/sitemap_path']; $siteMapId = 1; @@ -121,37 +171,83 @@ public function testTryToSaveInvalidDataShouldFailWithErrors() ->with('sitemap_id') ->willReturn($siteMapId); - $validator = $this->createMock($validatorClass); - $validator->expects($this->once()) + $this->pathValidator->expects($this->once()) ->method('setPaths') ->with($validPaths) ->willReturnSelf(); - $validator->expects($this->once()) + $this->pathValidator->expects($this->once()) ->method('isValid') ->with('/sitemap_path/sitemap_filename') ->willReturn(false); - $validator->expects($this->once()) + $this->pathValidator->expects($this->once()) ->method('getMessages') ->willReturn($messages); - $helper = $this->createMock($helperClass); - $helper->expects($this->once()) + $this->helper->expects($this->once()) ->method('getValidPaths') ->willReturn($validPaths); - $session = $this->createPartialMock($sessionClass, ['setFormData']); - $session->expects($this->once()) + $this->session->expects($this->once()) ->method('setFormData') ->with($data) ->willReturnSelf(); - $this->objectManagerMock->expects($this->once()) - ->method('create') - ->with($validatorClass) - ->willReturn($validator); - $this->objectManagerMock->expects($this->any()) - ->method('get') - ->willReturnMap([[$helperClass, $helper], [$sessionClass, $session]]); + $this->messageManagerMock->expects($this->at(0)) + ->method('addErrorMessage') + ->withConsecutive( + [$messages[0]], + [$messages[1]] + ) + ->willReturnSelf(); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('adminhtml/*/edit', ['sitemap_id' => $siteMapId]) + ->willReturnSelf(); + + $this->assertSame($this->resultRedirectMock, $this->saveController->execute()); + } + + public function testTryToSaveInvalidFileNameShouldFailWithErrors() + { + $validPaths = []; + $messages = ['message1', 'message2']; + $data = ['sitemap_filename' => 'sitemap_filename', 'sitemap_path' => '/sitemap_path']; + $siteMapId = 1; + + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('sitemap_id') + ->willReturn($siteMapId); + + $this->lengthValidator->expects($this->once()) + ->method('isValid') + ->with('sitemap_filename') + ->willReturn(false); + $this->lengthValidator->expects($this->once()) + ->method('getMessages') + ->willReturn($messages); + + $this->pathValidator->expects($this->once()) + ->method('setPaths') + ->with($validPaths) + ->willReturnSelf(); + $this->pathValidator->expects($this->once()) + ->method('isValid') + ->with('/sitemap_path/sitemap_filename') + ->willReturn(true); + + $this->helper->expects($this->once()) + ->method('getValidPaths') + ->willReturn($validPaths); + + $this->session->expects($this->once()) + ->method('setFormData') + ->with($data) + ->willReturnSelf(); $this->messageManagerMock->expects($this->at(0)) ->method('addErrorMessage') diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/EmailNotificationTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/EmailNotificationTest.php new file mode 100644 index 0000000000000..eafb47c086bac --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Unit/Model/EmailNotificationTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Sitemap\Test\Unit\Model; + +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Mail\TransportInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Translate\Inline\StateInterface; +use Magento\Sitemap\Model\EmailNotification; +use Magento\Sitemap\Model\Observer; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use PHPUnit\Framework\TestCase; + +/** + * Test for Magento\Sitemap\Model\EmailNotification + */ +class EmailNotificationTest extends TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var EmailNotification + */ + private $model; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var TransportBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $transportBuilderMock; + + /** + * @var StateInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $inlineTranslationMock; + + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + protected function setUp() + { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMock(); + $this->transportBuilderMock = $this->getMockBuilder(TransportBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + $this->inlineTranslationMock = $this->getMockBuilder(StateInterface::class) + ->getMock(); + + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject( + EmailNotification::class, + [ + 'inlineTranslation' => $this->inlineTranslationMock, + 'scopeConfig' => $this->scopeConfigMock, + 'transportBuilder' => $this->transportBuilderMock, + ] + ); + } + + public function testSendErrors() + { + $exception = 'Sitemap Exception'; + $transport = $this->createMock(TransportInterface::class); + + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with( + Observer::XML_PATH_ERROR_TEMPLATE, + ScopeInterface::SCOPE_STORE + ) + ->willReturn('error-recipient@example.com'); + + $this->inlineTranslationMock->expects($this->once()) + ->method('suspend'); + + $this->transportBuilderMock->expects($this->once()) + ->method('setTemplateIdentifier') + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('setTemplateOptions') + ->with([ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => Store::DEFAULT_STORE_ID, + ]) + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('setTemplateVars') + ->with(['warnings' => $exception]) + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('setFrom') + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('addTo') + ->will($this->returnSelf()); + + $this->transportBuilderMock->expects($this->once()) + ->method('getTransport') + ->willReturn($transport); + + $transport->expects($this->once()) + ->method('sendMessage'); + + $this->inlineTranslationMock->expects($this->once()) + ->method('resume'); + + $this->model->sendErrors(['Sitemap Exception']); + } +} diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php index ac88f23ff9d69..09f5418bbd762 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php @@ -5,10 +5,14 @@ */ namespace Magento\Sitemap\Test\Unit\Model; +use Magento\Framework\App\Area; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sitemap\Model\EmailNotification; +use Magento\Store\Model\App\Emulation; /** * Class ObserverTest + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ObserverTest extends \PHPUnit\Framework\TestCase @@ -33,21 +37,6 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ private $collectionFactoryMock; - /** - * @var \Magento\Framework\Mail\Template\TransportBuilder|\PHPUnit_Framework_MockObject_MockObject - */ - private $transportBuilderMock; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $storeManagerMock; - - /** - * @var \Magento\Framework\Translate\Inline\StateInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $inlineTranslationMock; - /** * @var \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection|\PHPUnit_Framework_MockObject_MockObject */ @@ -63,6 +52,16 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ private $objectManagerMock; + /** + * @var Emulation|\PHPUnit_Framework_MockObject_MockObject + */ + private $appEmulationMock; + + /** + * @var EmailNotification|\PHPUnit_Framework_MockObject_MockObject + */ + private $emailNotificationMock; + protected function setUp() { $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) @@ -74,28 +73,28 @@ protected function setUp() )->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->transportBuilderMock = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->getMock(); - $this->inlineTranslationMock = $this->getMockBuilder(\Magento\Framework\Translate\Inline\StateInterface::class) - ->getMock(); $this->sitemapCollectionMock = $this->createPartialMock( \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection::class, ['getIterator'] ); - $this->sitemapMock = $this->createPartialMock(\Magento\Sitemap\Model\Sitemap::class, ['generateXml']); - + $this->sitemapMock = $this->createPartialMock( + \Magento\Sitemap\Model\Sitemap::class, + [ + 'generateXml', + 'getStoreId', + ] + ); + $this->appEmulationMock = $this->createMock(Emulation::class); + $this->emailNotificationMock = $this->createMock(EmailNotification::class); $this->objectManager = new ObjectManager($this); + $this->observer = $this->objectManager->getObject( \Magento\Sitemap\Model\Observer::class, [ 'scopeConfig' => $this->scopeConfigMock, 'collectionFactory' => $this->collectionFactoryMock, - 'storeManager' => $this->storeManagerMock, - 'transportBuilder' => $this->transportBuilderMock, - 'inlineTranslation' => $this->inlineTranslationMock + 'appEmulation' => $this->appEmulationMock, + 'emailNotification' => $this->emailNotificationMock ] ); } @@ -103,7 +102,7 @@ protected function setUp() public function testScheduledGenerateSitemapsSendsExceptionEmail() { $exception = 'Sitemap Exception'; - $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); + $storeId = 1; $this->scopeConfigMock->expects($this->once())->method('isSetFlag')->willReturn(true); @@ -115,11 +114,15 @@ public function testScheduledGenerateSitemapsSendsExceptionEmail() ->method('getIterator') ->willReturn(new \ArrayIterator([$this->sitemapMock])); + $this->sitemapMock->expects($this->at(0)) + ->method('getStoreId') + ->willReturn($storeId); + $this->sitemapMock->expects($this->once()) ->method('generateXml') ->willThrowException(new \Exception($exception)); - $this->scopeConfigMock->expects($this->at(1)) + $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') ->with( \Magento\Sitemap\Model\Observer::XML_PATH_ERROR_RECIPIENT, @@ -127,44 +130,6 @@ public function testScheduledGenerateSitemapsSendsExceptionEmail() ) ->willReturn('error-recipient@example.com'); - $this->inlineTranslationMock->expects($this->once()) - ->method('suspend'); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateIdentifier') - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateOptions') - ->with([ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ]) - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateVars') - ->with(['warnings' => $exception]) - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('setFrom') - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('addTo') - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('getTransport') - ->willReturn($transport); - - $transport->expects($this->once()) - ->method('sendMessage'); - - $this->inlineTranslationMock->expects($this->once()) - ->method('resume'); - $this->observer->scheduledGenerateSitemaps(); } } diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index ccc3c65491422..19f104c9f3790 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -100,18 +100,24 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ private $eventManager; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory * @param \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory * @param \Magento\Config\Model\ResourceModel\Config\Data $configDataResource - * @param \Magento\Store\Model\Store $store - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param ResourceModel\Store\CollectionFactory $storeListFactory + * @param StoreManagerInterface $storeManager + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -125,13 +131,16 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - \Magento\Framework\Event\ManagerInterface $eventManager = null + \Magento\Framework\Event\ManagerInterface $eventManager = null, + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { $this->_configDataResource = $configDataResource; $this->_storeListFactory = $storeListFactory; $this->_storeManager = $storeManager; $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); parent::__construct( $context, $registry, @@ -244,6 +253,8 @@ public function getStoreCodes() } /** + * Get stores count + * * @return int */ public function getStoresCount() @@ -349,6 +360,8 @@ public function isCanDelete() } /** + * Get default store id + * * @return mixed */ public function getDefaultStoreId() @@ -365,6 +378,8 @@ public function setDefaultStoreId($defaultStoreId) } /** + * Get root category id + * * @return mixed */ public function getRootCategoryId() @@ -381,6 +396,8 @@ public function setRootCategoryId($rootCategoryId) } /** + * Get website id + * * @return mixed */ public function getWebsiteId() @@ -397,7 +414,7 @@ public function setWebsiteId($websiteId) } /** - * @return $this + * @inheritdoc */ public function beforeDelete() { @@ -445,6 +462,7 @@ public function afterSave() $this->_storeManager->reinitStores(); $this->eventManager->dispatch($this->_eventPrefix . '_save', ['group' => $group]); }); + $this->pillPut->put(); return parent::afterSave(); } @@ -473,7 +491,7 @@ public function getIdentities() } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -507,7 +525,7 @@ public function setCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -515,7 +533,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\GroupExtensionInterface $extensionAttributes @@ -524,7 +542,7 @@ public function setExtensionAttributes( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeType() @@ -533,7 +551,7 @@ public function getScopeType() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeTypeName() diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 29a1f4a9c666e..6814ad418bacc 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -33,6 +33,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Store extends AbstractExtensibleModel implements @@ -325,6 +326,11 @@ class Store extends AbstractExtensibleModel implements */ private $eventManager; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -351,6 +357,7 @@ class Store extends AbstractExtensibleModel implements * @param bool $isCustomEntryPoint * @param array $data optional generic object data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -379,7 +386,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, $isCustomEntryPoint = false, array $data = [], - \Magento\Framework\Event\ManagerInterface $eventManager = null + \Magento\Framework\Event\ManagerInterface $eventManager = null, + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -400,6 +408,8 @@ public function __construct( $this->websiteRepository = $websiteRepository; $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); parent::__construct( $context, $registry, @@ -706,7 +716,8 @@ protected function _updatePathUseRewrites($url) if ($this->_isCustomEntryPoint()) { $indexFileName = 'index.php'; } else { - $indexFileName = basename($_SERVER['SCRIPT_FILENAME']); + $scriptFilename = $this->_request->getServer('SCRIPT_FILENAME'); + $indexFileName = basename($scriptFilename); } $url .= $indexFileName . '/'; } @@ -897,7 +908,10 @@ public function setCurrentCurrencyCode($code) if (in_array($code, $this->getAvailableCurrencyCodes())) { $this->_getSession()->setCurrencyCode($code); - $defaultCode = $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); + $defaultCode = ($this->_storeManager->getStore() !== null) + ? $this->_storeManager->getStore()->getDefaultCurrency()->getCode() + : $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); + $this->_httpContext->setValue(Context::CONTEXT_CURRENCY, $code, $defaultCode); } return $this; @@ -1073,6 +1087,7 @@ public function afterSave() $this->getResource()->addCommitCallback(function () use ($event, $store) { $this->eventManager->dispatch($event, ['store' => $store]); }); + $this->pillPut->put(); return parent::afterSave(); } diff --git a/app/code/Magento/Store/Model/Website.php b/app/code/Magento/Store/Model/Website.php index c9a7d0013fe06..383b36fd63228 100644 --- a/app/code/Magento/Store/Model/Website.php +++ b/app/code/Magento/Store/Model/Website.php @@ -159,6 +159,11 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement */ protected $_currencyFactory; + /** + * @var \Magento\MessageQueue\Api\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -174,6 +179,7 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\MessageQueue\Api\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -190,7 +196,8 @@ public function __construct( \Magento\Directory\Model\CurrencyFactory $currencyFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\MessageQueue\Api\PoisonPillPutInterface $pillPut = null ) { parent::__construct( $context, @@ -208,10 +215,12 @@ public function __construct( $this->_websiteFactory = $websiteFactory; $this->_storeManager = $storeManager; $this->_currencyFactory = $currencyFactory; + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\MessageQueue\Api\PoisonPillPutInterface::class); } /** - * init model + * Init model * * @return void */ @@ -495,6 +504,8 @@ public function getWebsiteGroupStore() } /** + * Get default group id + * * @return mixed */ public function getDefaultGroupId() @@ -511,6 +522,8 @@ public function setDefaultGroupId($defaultGroupId) } /** + * Get code + * * @return mixed */ public function getCode() @@ -543,7 +556,7 @@ public function setName($name) } /** - * @return $this + * @inheritdoc */ public function beforeDelete() { @@ -581,7 +594,7 @@ public function afterSave() if ($this->isObjectNew()) { $this->_storeManager->reinitStores(); } - + $this->pillPut->put(); return parent::afterSave(); } @@ -635,8 +648,7 @@ public function getDefaultStore() } /** - * Retrieve default stores select object - * Select fields website_id, store_id + * Retrieve default stores select object, select fields website_id, store_id * * @param bool $withDefault include/exclude default admin website * @return \Magento\Framework\DB\Select @@ -671,7 +683,7 @@ public function getIdentities() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeType() @@ -680,7 +692,7 @@ public function getScopeType() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeTypeName() @@ -689,7 +701,7 @@ public function getScopeTypeName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -697,7 +709,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\WebsiteExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml index 91fe4fccddb91..870b92d17e679 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml @@ -25,4 +25,47 @@ <waitForElementVisible selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="waitForStoreGridReload"/> <see userInput="You saved the store." stepKey="seeSavedMessage" /> </actionGroup> + <actionGroup name="CreateCustomStore"> + <arguments> + <argument name="website" type="string"/> + <argument name="store" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <selectOption userInput="{{website}}" selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" stepKey="selectMainWebsite"/> + <fillField userInput="{{store}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{store}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{rootCategory}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="waitForStoreGridReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage"/> + </actionGroup> + <actionGroup name="AssertStoreGroupInGrid"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.nthRow('1')}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupInGridMessage"/> + </actionGroup> + <actionGroup name="AssertStoreGroupForm"> + <arguments> + <argument name="website" type="string"/> + <argument name="storeGroupName" type="string"/> + <argument name="storeGroupCode" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitTillAdminSystemStoreGroupPage"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website}}" stepKey="seeAssertWebsite"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupName"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroupCode}}" stepKey="seeAssertStoreGroupCode"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="{{rootCategory}}" stepKey="seeAssertRootCategory"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml index 6cbbb7ae22014..fda92810a13be 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml @@ -8,6 +8,7 @@ <!-- Test XML Example --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateStoreViewActionGroup"> <arguments> <argument name="StoreGroup" defaultValue="_defaultStoreGroup"/> @@ -15,6 +16,7 @@ </arguments> <amOnPage url="{{AdminSystemStoreViewPage.url}}" stepKey="navigateToNewStoreView"/> <waitForPageLoad stepKey="waitForPageLoad1" /> + <comment userInput="Creating Store View" stepKey="storeViewCreationComment" /> <!--Create Store View--> <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{StoreGroup.name}}" stepKey="selectStore" /> <fillField selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{customStore.name}}" stepKey="enterStoreViewName" /> @@ -22,19 +24,45 @@ <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="setStatus" /> <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarningAboutTakingALongTimeToComplete" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmModal" /> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForPageReload"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the store view." stepKey="seeSavedMessage" /> </actionGroup> + + <actionGroup name="AdminCreateStoreViewWithoutCheckActionGroup" extends="AdminCreateStoreViewActionGroup"> + <remove keyForRemoval="waitForPageReload"/> + <remove keyForRemoval="seeSavedMessage"/> + </actionGroup> + <!--Save the Store view--> <actionGroup name="AdminCreateStoreViewActionSaveGroup"> <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload2"/> <see userInput="You saved the store view." stepKey="seeSavedMessage" /> </actionGroup> - <!--Save the same Store view code for code validation--> - <actionGroup name="AdminCreateStoreViewCodeUniquenessActionGroup"> - <waitForLoadingMaskToDisappear stepKey="waitForForm"/> - <see userInput="Store with the same code already exists." stepKey="seeMessage" /> + + <actionGroup name="navigateToAdminContentManagementPage"> + <amOnPage url="{{AdminContentManagementPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + </actionGroup> + <actionGroup name="saveStoreConfiguration"> + <comment userInput="saveStoreConfiguration" stepKey="comment"/> + <waitForElementVisible selector="{{StoreConfigSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{StoreConfigSection.Save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <actionGroup name="saveStoreConfigurationAndValidateFieldError"> + <arguments> + <argument name="inputFieldError" type="string"/> + <argument name="errorMessageSelector" type="string"/> + <argument name="errorMessage" type="string"/> + </arguments> + <comment userInput="saveStoreConfigurationAndValidateFieldError" stepKey="comment"/> + <waitForElementVisible selector="{{StoreConfigSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{StoreConfigSection.Save}}" stepKey="clickSaveButton"/> + <waitForElement selector="{{inputFieldError}}" stepKey="waitForErrorField"/> + <waitForElementVisible selector="{{errorMessageSelector}}" stepKey="waitForErrorMessage"/> + <see selector="{{errorMessageSelector}}" userInput="{{errorMessage}}" stepKey="seeErrorMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml index ef8d77c8824ff..ca614ec24138c 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml @@ -36,4 +36,30 @@ <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabFromCurrentUrl"/> </actionGroup> + + <actionGroup name="AssertWebsiteInGrid"> + <arguments> + <argument name="websiteName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <seeElement selector="{{AdminStoresGridSection.websiteName(websiteName)}}" stepKey="seeAssertWebsiteInGrid"/> + </actionGroup> + + <actionGroup name="AssertWebsiteForm"> + <arguments> + <argument name="websiteName" type="string"/> + <argument name="websiteCode" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.websiteName(websiteName)}}" stepKey="clickWebsiteFirstRowInGrid"/> + <waitForPageLoad stepKey="waitTillWebsiteFormPageIsOpened"/> + <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabWebsiteIdFromCurrentUrl"/> + <seeInCurrentUrl url="/system_store/editWebsite/website_id/{$grabWebsiteIdFromCurrentUrl}" stepKey="seeWebsiteId"/> + <seeInField selector="{{AdminNewWebsiteSection.name}}" userInput="{{websiteName}}" stepKey="seeAssertWebsiteName"/> + <seeInField selector="{{AdminNewWebsiteSection.code}}" userInput="{{websiteCode}}" stepKey="seeAssertWebsiteCode"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml index 849dc91efedb7..cf2cabdcc2399 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -23,7 +23,38 @@ <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewAgain"/> <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreDelete"/> - <wait time="10" stepKey="extraWait"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> <see userInput="You deleted the store view." stepKey="seeDeleteMessage"/> </actionGroup> -</actionGroups> + <actionGroup name="DeleteCustomStoreViewBackupEnabledYesActionGroup"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickEditExistingStoreViewRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewButtonOnEditStorePage"/> + <selectOption userInput="Yes" selector="{{AdminStoreBackupOptionsSection.createBackupSelect}}" stepKey="setCreateDbBackupToYes"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewButtonOnDeleteStorePage"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreViewDelete"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="The database was backed up." stepKey="seeAssertDatabaseBackedUpMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="You deleted the store view." stepKey="seeAssertSuccessDeleteStoreViewMessage"/> + </actionGroup> + <actionGroup name="AssertStoreViewNotInGrid"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertStoreViewNotInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml index 58fd0a3f0bc2b..1721e3185402e 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml @@ -19,6 +19,7 @@ <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> <waitForPageLoad stepKey="waitForStoreToLoad"/> <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditWebsitePage"/> + <waitForPageLoad stepKey="waitForDeleteStoreGroupSectionLoad" time="30"/> <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteWebsiteButton"/> <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml index 023d5fc3587fb..1a7f24ed2aaa5 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml @@ -24,4 +24,24 @@ <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> <see userInput="You saved the store." stepKey="seeSavedMessage" /> </actionGroup> + + <actionGroup name="AdminAddCustomWebSiteToStoreGroup"> + <arguments> + <argument name="storeGroup" defaultValue="customStoreGroup"/> + <argument name="website" defaultValue="customWebsite"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroup.name}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{storeGroup.name}}" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="verifyThatCorrectStoreGroupFound"/> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreGroupPageLoad" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website.name}}" stepKey="selectWebsite" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="chooseRootCategory" /> + <click selector="{{AdminNewStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <conditionalClick selector="{{AdminNewStoreGroupSection.acceptNewStoreGroupCreation}}" dependentSelector="{{AdminNewStoreGroupSection.acceptNewStoreGroupCreation}}" visible="true" stepKey="clickAcceptNewStoreGroupCreationButton"/> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage" /> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml index ac8e9d717fdca..47236376f0209 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml @@ -18,6 +18,7 @@ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreSwitch"/> <waitForPageLoad stepKey="waitForStoreViewSwitched"/> + <scrollToTopOfPage stepKey="scrollToStoreSwitcher"/> <see userInput="{{storeView}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> </actionGroup> <actionGroup name="AdminSwitchToAllStoreViewActionGroup" extends="AdminSwitchStoreViewActionGroup"> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.xml index cfb2c7e6347c3..0960dfb47c368 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.xml @@ -11,6 +11,7 @@ <arguments> <argument name="website"/> </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> <click selector="{{AdminMainActionsSection.storeViewDropdown}}" stepKey="clickWebsiteSwitchDropdown"/> <waitForElementVisible selector="{{AdminMainActionsSection.websiteByName('Main Website')}}" stepKey="waitForWebsiteAreVisible"/> <click selector="{{AdminMainActionsSection.websiteByName(website.name)}}" stepKey="clickWebsiteByName"/> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreConfigurationBackendActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreConfigurationBackendActionGroup.xml new file mode 100644 index 0000000000000..38030f59a7a33 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreConfigurationBackendActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreConfigurationBackendActionGroup"> + <arguments> + <argument name="website" type="string"/> + <argument name="customStore" type="string"/> + <argument name="storeView1" type="string"/> + <argument name="storeView2" type="string"/> + </arguments> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="goToConfigStoreConfigurationPage"/> + <waitForPageLoad stepKey="waitForSystemStoreConfigurationPageLoad"/> + <click selector="{{AdminConfigSection.defaultConfigButton}}" stepKey="clickDefaultConfigButton"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{website}}" stepKey="seeAssertWebsiteInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{customStore}}" stepKey="seeAssertSecondStoreInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{storeView1}}" stepKey="seeAssertFirstStoreViewInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{storeView2}}" stepKey="seeAssertSecondStoreViewInDefaultConfigDropdown"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreFrontendActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreFrontendActionGroup.xml new file mode 100644 index 0000000000000..afd64e33e8b36 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreFrontendActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreFrontendActionGroup"> + <arguments> + <argument name="customStore" type="string"/> + <argument name="storeView1" type="string"/> + <argument name="storeView2" type="string"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickSwitchStoreButton"/> + <waitForElementVisible selector="{{StorefrontFooterSection.storeLink(customStore)}}" stepKey="waitForStoreLinkToVosible"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore)}}" stepKey="clickStoreLinkCustomStore"/> + <waitForPageLoad stepKey="waitForCustomStoreToLoad"/> + <see selector="{{StorefrontHeaderSection.storeViewName}}" userInput="{{storeView1}}" stepKey="seeAssertFirstStoreViewOnStorefront"/> + <click selector="{{StorefrontHeaderSection.storeViewName}}" stepKey="clickStoreViewName"/> + <see selector="{{StorefrontHeaderSection.storeViewDropdown}}" userInput="{{storeView2}}" stepKey="seeAssertSecondStoreViewOnStorefront"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupFormActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupFormActionGroup.xml new file mode 100644 index 0000000000000..a8b8b7bb6d07f --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupFormActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreGroupFormActionGroup"> + <arguments> + <argument name="website" type="string"/> + <argument name="storeGroupName" type="string"/> + <argument name="storeGroupCode" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitTillAdminSystemStoreGroupPage"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website}}" stepKey="seeAssertWebsite"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupName"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroupCode}}" stepKey="seeAssertStoreGroupCode"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="{{rootCategory}}" stepKey="seeAssertRootCategory"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupInGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupInGridActionGroup.xml new file mode 100644 index 0000000000000..fb36cbd0d1812 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupInGridActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreGroupInGridActionGroup"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.firstRow}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewFormActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewFormActionGroup.xml new file mode 100644 index 0000000000000..92ebbc3950131 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewFormActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreViewFormActionGroup"> + <arguments> + <argument name="storeDropdown" type="string"/> + <argument name="storeViewName" type="string"/> + <argument name="storeViewCode" type="string"/> + <argument name="status" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <seeInField selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{storeDropdown}}" stepKey="seeAssertStore"/> + <seeInField selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{storeViewName}}" stepKey="seeAssertStoreViewName"/> + <seeInField selector="{{AdminNewStoreSection.storeCodeTextField}}" userInput="{{storeViewCode}}" stepKey="seeAssertStoreViewCode"/> + <seeInField selector="{{AdminNewStoreSection.statusDropdown}}" userInput="{{status}}" stepKey="seeAssertStatus"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewInGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewInGridActionGroup.xml new file mode 100644 index 0000000000000..fad8eec9990cc --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewInGridActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreViewInGridActionGroup"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.firstRow}}" userInput="{{storeViewName}}" stepKey="seeAssertStoreViewInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStorefrontStoreCodeInUrlActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStorefrontStoreCodeInUrlActionGroup.xml new file mode 100644 index 0000000000000..3daa1c25a1737 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStorefrontStoreCodeInUrlActionGroup.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="AssertStorefrontStoreCodeInUrlActionGroup"> + <arguments> + <argument name="storeCode" type="string" defaultValue="{{_defaultStore.code}}" /> + </arguments> + <seeInCurrentUrl url="{{StorefrontHomePage.url}}{{storeCode}}" stepKey="seeStoreCodeInURL"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/ChangeStoreInStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/ChangeStoreInStoreViewActionGroup.xml new file mode 100644 index 0000000000000..15784235d5918 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/ChangeStoreInStoreViewActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ChangeStoreInStoreViewActionGroup"> + <arguments> + <argument name="storeDropdown" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{storeDropdown}}" stepKey="selectStoreGrpDropdown"/> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarningAboutTakingALongTimeToComplete"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmModal"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml index 31bbe7550e5a1..86d3963bc42b6 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml @@ -5,6 +5,7 @@ * 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="CreateCustomStoreViewActionGroup"> @@ -18,7 +19,24 @@ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> <selectOption userInput="{{customStore.is_active}}" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="selectStoreViewStatus"/> <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="clickAcceptNewStoreViewCreationButton"/> + </actionGroup> + <actionGroup name="CreateStoreView"> + <arguments> + <argument name="storeView" defaultValue="customStore"/> + <argument name="storeGroupName" defaultValue="_defaultStoreGroup.name"/> + <argument name="storeViewStatus" defaultValue="_defaultStore.is_active"/> + </arguments> + <amOnPage url="{{AdminSystemStoreViewPage.url}}" stepKey="amOnAdminSystemStoreViewPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <selectOption userInput="{{storeGroupName}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> + <fillField userInput="{{storeView.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="{{storeView.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption userInput="{{storeViewStatus}}" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="selectStoreViewStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationButton" /> <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="clickAcceptNewStoreViewCreationButton"/> + <see userInput="You saved the store view." stepKey="seeSavedMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml index 8e32b819aa954..8a1d830661aad 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml @@ -23,4 +23,33 @@ <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteStoreGroupButtonOnDeleteStorePage"/> </actionGroup> -</actionGroups> + <actionGroup name="DeleteCustomStoreBackupEnabledYesActionGroup"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="verifyThatCorrectStoreGroupFound"/> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteStoreGroupButtonOnEditStorePage"/> + <selectOption userInput="Yes" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> + <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteStoreGroupButtonOnDeleteStorePage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="The database was backed up." stepKey="seeAssertDatabaseBackedUpMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="You deleted the store." stepKey="seeAssertSuccessDeleteStoreGroupMessage"/> + </actionGroup> + <actionGroup name="AssertStoreNotInGrid"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertStoreGroupNotInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml index cc6a1fb62ea5f..da3ce02a80f28 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml @@ -13,15 +13,14 @@ </arguments> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> - <waitForPageLoad stepKey="waitForPageLoadAfterResetButtonClicked" time="10"/> <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton" /> - <waitForPageLoad stepKey="waitForPageLoadAfterSearch" time="10"/> <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> - + <waitForPageLoad stepKey="waitForPageLoadAfterWebsiteSelected" time="30"/> <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditStorePage"/> <selectOption userInput="No" selector="{{AdminStoresDeleteWebsiteSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteWebsiteSection.deleteButton}}" stepKey="clickDeleteButtonOnDeleteWebsitePage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the website." stepKey="checkSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/EditCustomStoreGroupAcceptWarningMessageActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/EditCustomStoreGroupAcceptWarningMessageActionGroup.xml new file mode 100644 index 0000000000000..8889795c8acbf --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/EditCustomStoreGroupAcceptWarningMessageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="EditCustomStoreGroupAcceptWarningMessageActionGroup" extends="CreateCustomStore"> + <arguments> + <argument name="website" type="string"/> + <argument name="store" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <remove keyForRemoval="selectCreateStore"/> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickFirstRow" before="selectMainWebsite"/> + <waitForPageLoad stepKey="waitForWarningMessageToAppear" before="seeAssertAlertWarningMessage" /> + <click selector="{{AdminStoreGroupActionsSection.okButton}}" stepKey="seeAssertAlertWarningMessage" before="waitForStoreGridReload"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml new file mode 100644 index 0000000000000..f11394c643ad7 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.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="StoreFrontProductValidationActionGroup"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url(product.urlKey)}}" stepKey="seeProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{product.name}}" stepKey="seeProductInStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{product.sku}}" stepKey="seeCorrectSku"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{product.price}}" stepKey="seeCorrectPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml index efd3a8c6b8cad..d30fc1e5a2a35 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml @@ -17,4 +17,9 @@ <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.code)}}" stepKey="clickSelectStoreView"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <actionGroup name="StorefrontSwitchDefaultStoreViewActionGroup" extends="StorefrontSwitchStoreViewActionGroup"> + <remove keyForRemoval="clickSelectStoreView"/> + <click selector="{{StorefrontHeaderSection.storeViewOption('default')}}" stepKey="clickSelectDefaultStoreView" after="waitForStoreViewDropdown"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml new file mode 100644 index 0000000000000..4333446925474 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml @@ -0,0 +1,24 @@ +<?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="StorefrontDisableAddStoreCodeToUrls"> + <!-- Magento default value --> + <data key="path">web/url/use_store</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontEnableAddStoreCodeToUrls"> + <data key="path">web/url/use_store</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index 9c418e9ebd496..4e043f9ff27db 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -39,6 +39,24 @@ <data key="store_type">group</data> <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> + <entity name="customStoreENNotUnique" type="store"> + <data key="name">EN</data> + <data key="code">en</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="customStoreNLNotUnique" type="store"> + <data key="name">NL</data> + <data key="code">nl</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> <entity name="staticStore" type="store"> <!--data key="group_id">customStoreGroup.id</data--> <data key="name">Second Store View</data> @@ -65,6 +83,80 @@ <data key="name" unique="suffix">StoreView</data> <data key="code" unique="suffix">StoreViewCode</data> </entity> + + <!-- For creation 10 Store Views--> + <entity name="storeViewData" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData1" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData2" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData3" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData4" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData5" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData6" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData7" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> <entity name="SecondStoreUnique" type="store"> <data key="name" unique="suffix">Second Store View </data> <data key="code" unique="suffix">second_store_view_</data> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml index 2127b5f3c8b21..7cbd686dea090 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml @@ -43,6 +43,19 @@ <data key="root_category_id">2</data> <data key="website_id">1</data> </entity> + <entity name="finnishStoreGroup" type="group"> + <data key="name">Finnish</data> + <data key="code">fin</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> + <entity name="swedishStoreGroup" type="group"> + <data key="name">Swedish</data> + <data key="code">swd</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> + <entity name="staticFirstStoreGroup" extends="staticStoreGroup"> <data key="name">NewStore</data> <data key="code">Base1</data> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml index 6e3e7ce7eb0d3..bc9746c132d4b 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml @@ -27,4 +27,18 @@ <entity name="disableFreeShipping" type="disableFreeShipping"> <data key="value">1</data> </entity> + + <entity name="MinimumOrderAmount90" type="minimum_order_amount"> + <requiredEntity type="free_shipping_subtotal">Price</requiredEntity> + </entity> + <entity name="Price" type="free_shipping_subtotal"> + <data key="value">90</data> + </entity> + + <entity name="DefaultMinimumOrderAmount" type="minimum_order_amount"> + <requiredEntity type="free_shipping_subtotal">DefaultPrice</requiredEntity> + </entity> + <entity name="DefaultPrice" type="free_shipping_subtotal"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml index f636336524f01..ae605256a2819 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -23,4 +23,8 @@ <data key="name" unique="suffix">Custom Website</data> <data key="code" unique="suffix">custom_website</data> </entity> -</entities> + <entity name="updateCustomWebsite" extends="customWebsite"> + <data key="name" unique="suffix">website_upd</data> + <data key="code" unique="suffix">code_upd</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml index 83288ecfdaf71..6f88bca760204 100644 --- a/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml @@ -34,5 +34,16 @@ </object> </operation> + <operation name="MinimumOrderAmount" dataType="minimum_order_amount" type="create" auth="adminFormKey" url="/admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="minimum_order_amount"> + <object key="freeshipping" dataType="minimum_order_amount"> + <object key="fields" dataType="minimum_order_amount"> + <object key="free_shipping_subtotal" dataType="free_shipping_subtotal"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> </operations> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml index ea5d9aab8b26d..fb98c66983776 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml @@ -11,5 +11,6 @@ <element name="storeGrpNameTextField" type="input" selector="#group_name"/> <element name="storeGrpCodeTextField" type="input" selector="#group_code"/> <element name="storeRootCategoryDropdown" type="select" selector="#group_root_category_id"/> + <element name="acceptNewStoreGroupCreation" type="button" selector=".action-primary.action-accept" /> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml index f79ea080ed1ca..9ad0f40cc4d01 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml @@ -8,5 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminStoreGroupActionsSection"> <element name="saveButton" type="button" selector="#save" timeout="60" /> + <element name="okButton" type="button" selector="//footer[@class='modal-footer']//button[@class='action-primary action-accept']/span" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml index fea7dc07c8287..1bdf7f0c22c4e 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml @@ -8,6 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminStoresDeleteWebsiteSection"> <element name="createDbBackup" type="select" selector="#store_create_backup"/> - <element name="deleteButton" type="button" selector="#delete" timeout="30"/> + <element name="deleteButton" type="button" selector="#delete" timeout="120"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml index 1b84027a5dd4a..e8333bd307530 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -14,12 +14,17 @@ <section name="AdminStoresGridSection"> <element name="storeGrpFilterTextField" type="input" selector="#storeGrid_filter_group_title"/> <element name="websiteFilterTextField" type="input" selector="#storeGrid_filter_website_title"/> - <element name="storeFilterTextField" type="input" selector="#storeGrid_filter_store_title"/> + <element name="storeFilterTextField" type="input" selector="#storeGrid_filter_store_title" timeout="90"/> <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]" timeout="30"/> <element name="resetButton" type="button" selector="button[title='Reset Filter']" timeout="30"/> <element name="websiteNameInFirstRow" type="text" selector=".col-website_title>a"/> <element name="storeGrpNameInFirstRow" type="text" selector=".col-group_title>a"/> <element name="storeNameInFirstRow" type="text" selector=".col-store_title>a"/> <element name="firstRow" type="textarea" selector="(//*[@id='storeGrid_table']/tbody/tr)[1]"/> + <element name="nthRow" type="textarea" selector="(//*[@id='storeGrid_table']/tbody/tr)[{{rownum}}]" parameterized="true"/> + <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div"/> + <element name="emptyText" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> + <element name="websiteName" type="text" selector="//td[@class='a-left col-website_title ']/a[contains(.,'{{websiteName}}')]" parameterized="true"/> + <element name="gridCell" type="text" selector="//table[@class='data-grid']//tr[{{row}}]//td[count(//table[@class='data-grid']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml index 98ad1db46732b..e40aa76967bec 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml @@ -10,7 +10,7 @@ <element name="createStoreViewButton" type="button" selector="#add_store" timeout="30"/> <element name="createStoreButton" type="button" selector="#add_group" timeout="30"/> <element name="createWebsiteButton" type="button" selector="#add" timeout="30"/> - <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="saveButton" type="button" selector="#save" timeout="90"/> <element name="backButton" type="button" selector="#back" timeout="30"/> <element name="deleteButton" type="button" selector="#delete" timeout="30"/> </section> diff --git a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml index af18e858e1057..416808e58e6b3 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,7 +9,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> <element name="storeViewSwitcher" type="button" selector="#switcher-language-trigger"/> - <element name="storeViewDropdown" type="button" selector="ul.switcher-dropdown"/> + <element name="storeViewDropdown" type="button" selector=".active ul.switcher-dropdown"/> <element name="storeViewOption" type="button" selector="li.view-{{var1}}>a" parameterized="true"/> + <element name="storeView" type="button" selector="//div[@class='actions dropdown options switcher-options active']//ul//li//a[contains(text(),'{{var}}')]" parameterized="true"/> + <element name="storeViewList" type="button" selector="//li[contains(.,'{{storeViewName}}')]//a" parameterized="true"/> + <element name="storeViewName" type="text" selector="//*[@id='switcher-language-trigger']//span"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml new file mode 100644 index 0000000000000..8e8f31eaca865 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest"> + <annotations> + <stories value="Create Store Group"/> + <title value="Create Store Group with Custom Website and Default Category"/> + <description value="Test log in to Stores and Create Store Group with Custom Website and Default Category Test"/> + <testCaseId value="MC-14300"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create custom store group with custom website and default category and verify AssertStoreGroupSuccessSaveMessage--> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStoreGroup"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + + <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid message--> + <actionGroup ref="AssertStoreGroupInGrid" stepKey="seeCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Go to store group form page and verify AssertStoreGroupForm--> + <actionGroup ref="AssertStoreGroupForm" stepKey="seeCreatedStoreGroupForm"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + <!--Also verify absence of delete button on store group form page(AssertStoreGroupNoDeleteButton)--> + <dontSee selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="AssertStoreGroupNoDeleteButton"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml new file mode 100644 index 0000000000000..18f9822145dec --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest"> + <annotations> + <stories value="Create Store Group"/> + <title value="Create Store Group with Custom Website and Root Category"/> + <description value="Test log in to Stores and Create Store Group with Custom Website and Root Category Test"/> + <testCaseId value="MC-14299"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create root category--> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <!--Delete website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <!--Delete root category--> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create custom store group with custom website and root category and verify AssertStoreGroupSuccessSaveMessage--> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStoreGroup"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + + <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGrid" stepKey="seeCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Go to store group form page and verify AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm--> + <actionGroup ref="AssertStoreGroupForm" stepKey="seeCreatedStoreGroupInForm"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml new file mode 100644 index 0000000000000..ddc5d061c1db2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest"> + <annotations> + <stories value="Create Store Group"/> + <title value="Create Store Group with Default Website and Default Category"/> + <description value="Test log in to Stores and Create Store Group with Default Website and Default Category Test"/> + <testCaseId value="MC-14298"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStoreGroup"> + <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create custom store group with default website and default category and verify AssertStoreGroupSuccessSaveMessage--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + + <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGrid" stepKey="seeCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + </actionGroup> + + <!--Go to store group form page and verify AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm--> + <actionGroup ref="AssertStoreGroupForm" stepKey="seeCreatedStoreGroupForm"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml index e7a3d03f337db..e20eb70ae6f45 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml @@ -11,29 +11,36 @@ <annotations> <features value="Store"/> <stories value="Create a store view in admin"/> - <title value="Admin should be able to create a store view"/> - <description value="Admin should be able to create a store view"/> + <title value="Admin shouldn't be able to create a Store View with the same code"/> + <description value="Admin shouldn't be able to create a Store View with the same code"/> <group value="storeView"/> <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-95111"/> + <useCaseId value="MAGETWO-95111"/> + <testCaseId value="MC-15422"/> </annotations> + <before> - <actionGroup ref="LoginActionGroup" stepKey="login"/> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> - <!--<createData stepKey="b2" entity="customStoreGroup"/>--> </before> - <!--Save store view on Store Grid--> - <actionGroup ref="AdminCreateStoreViewActionSaveGroup" stepKey="createStoreViewSave" /> - <!--Confirm new store view created on Store Grid--> - <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilter"/> - <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch" /> - <waitForPageLoad stepKey="waitForPageLoad"/> - <see selector="{{AdminStoresGridSection.storeNameInFirstRow}}" userInput="{{customStore.name}}" stepKey="seeNewStoreView" /> - <!--Creating the same store view to validate the code uniqueness on store form--> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2" /> - <actionGroup ref="AdminCreateStoreViewCodeUniquenessActionGroup" stepKey="createStoreViewCode" /> + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> <actionGroup ref="logout" stepKey="logout"/> </after> + + <!--Filter grid and see created store view--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch"/> + <see selector="{{AdminStoresGridSection.gridCell('1', 'Store View')}}" userInput="{{customStore.name}}" stepKey="seeNewStoreView"/> + <!--Try to create store view with the same code--> + <actionGroup ref="AdminCreateStoreViewWithoutCheckActionGroup" stepKey="createSameStoreView"/> + <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSuccessMessage"/> + <see selector="{{AdminMessagesSection.error}}" userInput="Store with the same code already exists." stepKey="seeErrorMessage"/> </test> </tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml new file mode 100644 index 0000000000000..1608d0b7b5a25 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateWebsiteTest"> + <annotations> + <stories value="Create Website"/> + <title value="Create Website and Verify Store Form"/> + <description value="Test log in to Stores and Create Website Test"/> + <testCaseId value="MC-14302"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create website and AssertWebsiteSuccessSaveMessage--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + + <!--Search created website in grid and verify AssertWebsiteInGrid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeWebsiteInGrid"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Verify website name and websitecode on website form (AssertWebsiteForm and AssertWebsiteOnStoreForm)--> + <actionGroup ref="AssertWebsiteForm" stepKey="seeWebsiteForm"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml new file mode 100644 index 0000000000000..652537f7864cd --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteStoreGroupTest"> + <annotations> + <stories value="Delete Store Group"/> + <title value="Delete store group and save backup"/> + <description value="Test log in to Stores and Delete Store Group Test"/> + <testCaseId value="MC-14297"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store group--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStore.name}}"/> + <argument name="storeGroupCode" value="{{customStore.code}}"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete custom store group and verify AssertStoreGroupSuccessDeleteAndBackupMessages--> + <actionGroup ref="DeleteCustomStoreBackupEnabledYesActionGroup" stepKey="deleteCustomStoreGroup"> + <argument name="storeGroupName" value="{{customStore.name}}"/> + </actionGroup> + + <!--Verify deleted Store group is not present in grid and verify AssertStoreGroupNotInGrid message--> + <actionGroup ref="AssertStoreNotInGrid" stepKey="verifyDeletedStoreGroupNotInGrid"> + <argument name="storeGroupName" value="{{customStore.name}}"/> + </actionGroup> + + <!--Go to backup index page and verify AssertBackupInGrid--> + <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupIndexPage"/> + <waitForPageLoad stepKey="waitForBackupIndexPageLoad"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{WebSetupWizardBackup.name}}" stepKey="seeBackupInGrid"/> + <!--Delete database backup--> + <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> + <argument name="backup" value="WebSetupWizardBackup"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml new file mode 100644 index 0000000000000..fc1dcb5ee1a24 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteStoreViewTest"> + <annotations> + <stories value="Delete Store View"/> + <title value="Delete Store View and Save Backup"/> + <description value="Test log in to Stores and Delete Store View"/> + <testCaseId value="MC-14303"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store view--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete custom store view and verify AssertStoreSuccessDeleteMessage And BackupMessage--> + <actionGroup ref="DeleteCustomStoreViewBackupEnabledYesActionGroup" stepKey="deleteCustomStoreView"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + + <!--Verify deleted store view not present in grid and verify AssertStoreNotInGrid Message--> + <actionGroup ref="AssertStoreViewNotInGrid" stepKey="verifyDeletedStoreViewNotInGrid"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + + <!--Go to backup index page and verify AssertBackupInGrid--> + <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupIndexPage"/> + <waitForPageLoad stepKey="waitForBackupIndexPageLoad"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{WebSetupWizardBackup.name}}" stepKey="seeBackupInGrid"/> + <!--Delete database backup--> + <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> + <argument name="backup" value="WebSetupWizardBackup"/> + </actionGroup> + + <!--Go to storefront and verify AssertStoreNotOnFrontend--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <dontSee selector="{{StorefrontHeaderSection.storeViewList(storeViewData.name)}}" stepKey="dontSeeAssertStoreViewNameOnStorefront"/> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml new file mode 100644 index 0000000000000..b86b99936dbe2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveStoreToOtherGroupSameWebsiteTest"> + <annotations> + <stories value="Move Store"/> + <title value="Move Store To Other Group Same Website and Verify Backend and Frontend"/> + <description value="Test log in to Stores and Move Store To Other Group Same Website Test"/> + <testCaseId value="MC-14294"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create first store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createFirstStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStore.name}}"/> + <argument name="storeGroupCode" value="{{customStore.code}}"/> + </actionGroup> + <!-- Create first store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="storeViewData1"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="storeViewData2"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteFirstStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteSecondStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created second store view in grid--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchCreatedStoreViewInGrid"> + <argument name="storeViewName" value="{{storeViewData2.name}}"/> + </actionGroup> + <!--Move created store view to other store keeping website same--> + <actionGroup ref="ChangeStoreInStoreViewActionGroup" stepKey="moveStoreView"> + <argument name="storeDropdown" value="{{customStore.name}}"/> + </actionGroup> + <!--Save the above store view and verify AssertStoreViewSuccessSaveMessage--> + <actionGroup ref="AdminCreateStoreViewActionSaveGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"/> + + <!--Search moved store view(from above step) in grid and verify AssertStoreInGrid--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchMovedStoreViewInGrid"> + <argument name="storeViewName" value="{{storeViewData2.name}}"/> + </actionGroup> + + <!--Go to store view form page and verify AssertStoreForm--> + <actionGroup ref="AssertStoreViewFormActionGroup" stepKey="verifyStoreViewForm"> + <argument name="storeDropdown" value="{{customStore.name}}"/> + <argument name="storeViewName" value="{{storeViewData2.name}}"/> + <argument name="storeViewCode" value="{{storeViewData2.code}}"/> + <argument name="status" value="Enabled"/> + </actionGroup> + + <!--Go to store configuration page and verify AssertStoreBackend--> + <actionGroup ref="AssertStoreConfigurationBackendActionGroup" stepKey="verifyValuesOnStoreBackend"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="customStore" value="{{customStore.name}}"/> + <argument name="storeView1" value="{{storeViewData1.name}}"/> + <argument name="storeView2" value="{{storeViewData2.name}}"/> + </actionGroup> + + <!--Go to storefront and verify AssertStoreFrontend--> + <actionGroup ref="AssertStoreFrontendActionGroup" stepKey="verifyValuesOnStoreFrontend"> + <argument name="customStore" value="{{customStore.name}}"/> + <argument name="storeView1" value="{{storeViewData1.name}}"/> + <argument name="storeView2" value="{{storeViewData2.name}}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml new file mode 100644 index 0000000000000..9c84388d86f99 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest"> + <annotations> + <stories value="Update Store Group"/> + <title value="Update Store Group, Accept Alert and Verify Store View Form"/> + <description value="Test log in to Stores and Update Store Group Test"/> + <testCaseId value="MC-14296"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create root category--> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <!--Create custom store group--> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{staticStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + </before> + <after> + <!--Delete website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <!--Delete root category--> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open created Store group in grid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{staticStoreGroup.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <!--Update created Store group as per requirement and accept alert message--> + <actionGroup ref="EditCustomStoreGroupAcceptWarningMessageActionGroup" stepKey="updateCustomStoreGroup"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + + <!--Search updated store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeUpdatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm)--> + <actionGroup ref="AssertStoreGroupFormActionGroup" stepKey="seeUpdatedStoreGroupForm"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml new file mode 100644 index 0000000000000..3d85a34901434 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateStoreGroupAndVerifyStoreViewFormTest"> + <annotations> + <stories value="Update Store Group"/> + <title value="Update Store Group and Verify Store View Form"/> + <description value="Test log in to Stores and Update Store Group Test"/> + <testCaseId value="MC-14295"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store group--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStoreGroup"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteUpdatedStoreGroup"> + <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open created Store group in grid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <!--Update created Store group as per requirement--> + <actionGroup ref="CreateCustomStore" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + + <!--Search updated store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeUpdatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm)--> + <actionGroup ref="AssertStoreGroupFormActionGroup" stepKey="seeUpdatedStoreGroupForm"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml new file mode 100644 index 0000000000000..054ee789fbdc5 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateStoreViewTest"> + <annotations> + <stories value="Update Store View"/> + <title value="Update Store View and Verify Backend and Frontend"/> + <description value="Test log in to Stores and Update Store View Test"/> + <testCaseId value="MC-14316"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store view--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteUpdatedStoreView"> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created store view in grid--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchCreatedStoreViewInGrid"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <!--Update created store view as per requirements--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="updateStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + <!--Save the updated Store view and verify AssertStoreViewSuccessSaveMessage--> + <actionGroup ref="AdminCreateStoreViewActionSaveGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"> + </actionGroup> + + <!--Search updated store view in grid and verify AssertStoreViewInGridMessage--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="verifyUpdatedStoreViewInGrid"> + <argument name="storeViewName" value="{{SecondStoreUnique.name}}"/> + </actionGroup> + + <!--Go to store view form page and verify AssertStoreForm--> + <actionGroup ref="AssertStoreViewFormActionGroup" stepKey="verifyStoreViewForm"> + <argument name="storeDropdown" value="{{_defaultStoreGroup.name}}"/> + <argument name="storeViewName" value="{{SecondStoreUnique.name}}"/> + <argument name="storeViewCode" value="{{SecondStoreUnique.code}}"/> + <argument name="status" value="Enabled"/> + </actionGroup> + + <!--Go to store configuration page and verify AssertStoreBackend--> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="goToConfigStoreConfigurationPage"/> + <waitForPageLoad stepKey="waitForSystemStoreConfigurationPageLoad" /> + <click selector="{{AdminConfigSection.defaultConfigButton}}" stepKey="clickDefaultConfigButton"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{storeViewData.name}}" stepKey="seeAssertStoreViewInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{SecondStoreUnique.name}}" stepKey="seeAssertUpdateStoreViewInDefaultConfigDropdown"/> + + <!--Go to storefront and verify AssertStoreFrontend--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="selectStoreSwitcher"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <see selector="{{StorefrontHeaderSection.storeViewDropdown}}" userInput="{{storeViewData.name}}" stepKey="seeAssertStoreViewOnStorefront"/> + <see selector="{{StorefrontHeaderSection.storeViewDropdown}}" userInput="{{SecondStoreUnique.name}}" stepKey="seeAssertUpdatedStoreViewOnStorefront"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml new file mode 100644 index 0000000000000..6b666126569ae --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateWebsiteTest"> + <annotations> + <stories value="Update Website"/> + <title value="Update Website and Verify Store Form"/> + <description value="Test log in to Stores and Update Website Test"/> + <testCaseId value="MC-14301"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created custom website in grid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeWebsiteInGrid"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.websiteName(customWebsite.name)}}" stepKey="clickWebsiteFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForWebsiteFormPageToOpen"/> + <!--Update website name and website code as per data created and verify AssertWebsiteSuccessSaveMessage--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{updateCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{updateCustomWebsite.code}}"/> + </actionGroup> + + <!--Search updated custom website(from above step) in grid and verify AssertWebsiteInGrid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeUpdatedWebsiteInGrid"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertWebsiteForm and AssertWebsiteOnStoreForm)--> + <actionGroup ref="AssertWebsiteForm" stepKey="seeUpdatedWebsiteForm"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{updateCustomWebsite.code}}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml new file mode 100644 index 0000000000000..95f5a9cd2d669 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml @@ -0,0 +1,30 @@ +<?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="StorefrontAddStoreCodeInUrlTest"> + <annotations> + <features value="Backend"/> + <title value="Store code should be added to storefront URL if the corresponding configuration is enabled"/> + <description value="Store code should be added to storefront URL if the corresponding configuration is enabled"/> + <testCaseId value="MC-15944" /> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + </after> + + <actionGroup ref="StorefrontClickOnHeaderLogoActionGroup" stepKey="clickOnStorefrontHeaderLogo"/> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeStoreCodeInUrl"/> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php index f4a5010e51b88..a83ca833a15e0 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php @@ -160,7 +160,7 @@ public function testGetWebsite() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['websiteRepository' => $websiteRepository,] + ['websiteRepository' => $websiteRepository] ); $model->setWebsiteId($websiteId); @@ -181,7 +181,7 @@ public function testGetWebsiteIfWebsiteIsNotExist() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['websiteRepository' => $websiteRepository,] + ['websiteRepository' => $websiteRepository] ); $model->setWebsiteId(null); @@ -207,7 +207,7 @@ public function testGetGroup() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['groupRepository' => $groupRepository,] + ['groupRepository' => $groupRepository] ); $model->setGroupId($groupId); @@ -228,7 +228,7 @@ public function testGetGroupIfGroupIsNotExist() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['groupRepository' => $groupRepository,] + ['groupRepository' => $groupRepository] ); $model->setGroupId(null); @@ -377,30 +377,31 @@ public function testGetBaseUrlEntryPoint() $configMock = $this->getMockForAbstractClass(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $configMock->expects($this->atLeastOnce()) ->method('getValue') - ->will($this->returnCallback( - function ($path, $scope, $scopeCode) use ($expectedPath) { - return $expectedPath == $path ? 'http://domain.com/' . $path . '/' : null; - } - )); + ->willReturnCallback(function ($path, $scope, $scopeCode) use ($expectedPath) { + return $expectedPath == $path ? 'http://domain.com/' . $path . '/' : null; + }); + $this->requestMock->expects($this->once()) + ->method('getServer') + ->with('SCRIPT_FILENAME') + ->willReturn('test_script.php'); + /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, [ 'config' => $configMock, 'isCustomEntryPoint' => false, + 'request' => $this->requestMock ] ); $model->setCode('scopeCode'); $this->setUrlModifier($model); - $server = $_SERVER; - $_SERVER['SCRIPT_FILENAME'] = 'test_script.php'; $this->assertEquals( $expectedBaseUrl, $model->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_LINK, false) ); - $_SERVER = $server; } /** @@ -592,7 +593,7 @@ public function testGetAllowedCurrencies() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['config' => $configMock, 'currencyInstalled' => $currencyPath,] + ['config' => $configMock, 'currencyInstalled' => $currencyPath] ); $this->assertEquals($expectedResult, $model->getAllowedCurrencies()); diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index ebaa32b95f48b..da408f105ccb6 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-message-queue": "*", "magento/module-catalog": "*", "magento/module-config": "*", "magento/module-directory": "*", diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index af79d0e3e28b7..d9f7eaaaa294c 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -6,10 +6,10 @@ type Query { type Website @doc(description: "The type contains information about a website") { id : Int @doc(description: "The ID number assigned to the website") - name : String @doc(description: "The website name. Websites use this name to identify it easyer.") + name : String @doc(description: "The website name. Websites use this name to identify it easier.") code : String @doc(description: "A code assigned to the website to identify it") sort_order : Int @doc(description: "The attribute to use for sorting websites") - default_group_id : String @doc(description: "The default group id that the website has") + default_group_id : String @doc(description: "The default group ID that the website has") is_default : Boolean @doc(description: "Specifies if this is the default website") } diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php index e13373fb72558..2e4980c2fbfd0 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php @@ -8,7 +8,8 @@ use Magento\Catalog\Block\Product\Context; use Magento\Catalog\Helper\Product as CatalogProduct; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Image\UrlBuilder; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Layer\Category as CategoryLayer; use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; @@ -39,6 +40,11 @@ class Configurable extends \Magento\Swatches\Block\Product\Renderer\Configurable */ private $variationPrices; + /** + * @var \Magento\Catalog\Model\Layer\Resolver + */ + private $layerResolver; + /** * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @param Context $context @@ -55,6 +61,7 @@ class Configurable extends \Magento\Swatches\Block\Product\Renderer\Configurable * @param SwatchAttributesProvider|null $swatchAttributesProvider * @param \Magento\Framework\Locale\Format|null $localeFormat * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices + * @param Resolver $layerResolver */ public function __construct( Context $context, @@ -70,7 +77,8 @@ public function __construct( array $data = [], SwatchAttributesProvider $swatchAttributesProvider = null, \Magento\Framework\Locale\Format $localeFormat = null, - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null, + Resolver $layerResolver = null ) { parent::__construct( $context, @@ -92,10 +100,11 @@ public function __construct( $this->variationPrices = $variationPrices ?: ObjectManager::getInstance()->get( \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class ); + $this->layerResolver = $layerResolver ?: ObjectManager::getInstance()->get(Resolver::class); } /** - * @return string + * @inheritdoc */ protected function getRendererTemplate() { @@ -121,7 +130,7 @@ protected function _toHtml() } /** - * @return array + * @inheritdoc */ protected function getSwatchAttributesData() { @@ -183,6 +192,7 @@ protected function getOptionImages() * Add images to result json config in case of Layered Navigation is used * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @since 100.2.0 */ protected function _getAdditionalConfig() @@ -247,4 +257,16 @@ private function getLayeredAttributesIfExists(Product $configurableProduct, arra return $layeredAttributes; } + + /** + * @inheritdoc + */ + public function getCacheKeyInfo() + { + $cacheKeyInfo = parent::getCacheKeyInfo(); + /** @var CategoryLayer $catalogLayer */ + $catalogLayer = $this->layerResolver->get(); + $cacheKeyInfo[] = $catalogLayer->getStateKey(); + return $cacheKeyInfo; + } } diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index 69217ea377796..f9a600925b2a9 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -254,18 +254,15 @@ public function loadVariationByFallback(Product $parentProduct, array $attribute $this->addFilterByParent($productCollection, $parentId); $configurableAttributes = $this->getAttributesFromConfigurable($parentProduct); - $allAttributesArray = []; + + $resultAttributesToFilter = []; foreach ($configurableAttributes as $attribute) { - if (!empty($attribute['default_value'])) { - $allAttributesArray[$attribute['attribute_code']] = $attribute['default_value']; + $attributeCode = $attribute->getData('attribute_code'); + if (array_key_exists($attributeCode, $attributes)) { + $resultAttributesToFilter[$attributeCode] = $attributes[$attributeCode]; } } - $resultAttributesToFilter = array_merge( - $attributes, - array_diff_key($allAttributesArray, $attributes) - ); - $this->addFilterByAttributes($productCollection, $resultAttributesToFilter); $variationProduct = $productCollection->getFirstItem(); diff --git a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php index 9dc5b3a0c816f..9ad62265be21f 100644 --- a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php +++ b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php @@ -7,8 +7,9 @@ namespace Magento\Swatches\Model\ResourceModel; /** - * @codeCoverageIgnore * Swatch Resource Model + * + * @codeCoverageIgnore * @api * @since 100.0.2 */ @@ -25,8 +26,10 @@ protected function _construct() } /** - * @param string $defaultValue + * Update default swatch option value. + * * @param integer $id + * @param string $defaultValue * @return void */ public function saveDefaultSwatchOption($id, $defaultValue) @@ -49,7 +52,7 @@ public function clearSwatchOptionByOptionIdAndType($optionIDs, $type = null) { if (count($optionIDs)) { foreach ($optionIDs as $optionId) { - $where = ['option_id' => $optionId]; + $where = ['option_id = ?' => $optionId]; if ($type !== null) { $where['type = ?'] = $type; } diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml index 60a8035dedeca..2c91bba75fec9 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml @@ -62,4 +62,21 @@ <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> </actionGroup> + <actionGroup name="AddVisualSwatchToProductWithStorefrontConfigActionGroup" extends="AddVisualSwatchToProductActionGroup"> + <arguments> + <argument name="attribute" defaultValue="visualSwatchAttribute"/> + <argument name="option1" defaultValue="visualSwatchOption1"/> + <argument name="option2" defaultValue="visualSwatchOption2"/> + </arguments> + + <!-- Go to Storefront Properties tab --> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillDefaultStoreLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" stepKey="waitTabLoad" after="goToStorefrontPropertiesTab"/> + <selectOption selector="{{AdminNewAttributePanel.useInSearch}}" stepKey="switchOnUsInSearch" userInput="Yes" after="waitTabLoad"/> + <selectOption selector="{{AdminNewAttributePanel.visibleInAdvancedSearch}}" stepKey="switchOnVisibleInAdvancedSearch" userInput="Yes" after="switchOnUsInSearch"/> + <selectOption selector="{{AdminNewAttributePanel.comparableOnStorefront}}" stepKey="switchOnComparableOnStorefront" userInput="Yes" after="switchOnVisibleInAdvancedSearch"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" stepKey="selectUseInLayer" userInput="Filterable (with results)" after="switchOnComparableOnStorefront"/> + <selectOption selector="{{AdminNewAttributePanel.visibleOnCatalogPagesOnStorefront}}" stepKey="switchOnVisibleOnCatalogPagesOnStorefront" userInput="Yes" after="selectUseInLayer"/> + <selectOption selector="{{AdminNewAttributePanel.useInProductListing}}" stepKey="switchOnUsedInProductListing" userInput="Yes" after="switchOnVisibleOnCatalogPagesOnStorefront"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 415ae88fceb52..4290ddbbd8dd4 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -9,10 +9,11 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductInfoMainSection"> - <element name="swatchOptionByLabel" type="button" selector="div.swatch-option[option-label={{opt}}]" parameterized="true"/> + <element name="swatchOptionByLabel" type="button" selector="div.swatch-option[option-label='{{opt}}']" parameterized="true"/> <element name="nthSwatchOption" type="button" selector="div.swatch-option:nth-of-type({{var}})" parameterized="true"/> <element name="selectedSwatchValue" type="text" selector="//div[contains(@class, 'swatch-attribute') and contains(., '{{attr}}')]//span[contains(@class, 'swatch-attribute-selected-option')]" parameterized="true"/> <element name="swatchAttributeOptions" type="text" selector="div.swatch-attribute-options"/> <element name="nthSwatchOptionText" type="button" selector="div.swatch-option.text:nth-of-type({{n}})" parameterized="true"/> + <element name="productSwatch" type="button" selector="//div[@class='swatch-option'][@aria-label='{{var1}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index e4c96ab3a2ba7..5347a1a1f870f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -79,16 +79,25 @@ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> <waitForPageLoad time="30" stepKey="waitForProductGrid"/> <actionGroup ref="goToCreateProductPage" stepKey="goToCreateConfigurableProduct"> - <argument name="product" value="BaseConfigurableProduct"/> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <!-- Add image to configurable product --> + <actionGroup ref="addProductImage" stepKey="addFirstImageForProductConfigurable"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <!-- Add image to configurable product --> + <actionGroup ref="addProductImage" stepKey="addSecondImageForProductConfigurable"> + <argument name="image" value="TestImageNew"/> </actionGroup> <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> - <argument name="product" value="BaseConfigurableProduct"/> + <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> <!-- Create configurations based off the visual swatch we created earlier --> - <actionGroup ref="createConfigurationsForAttribute" stepKey="createConfigurations"> + <actionGroup ref="createConfigurationsForAttributeWithImages" stepKey="createConfigurations"> <argument name="attributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="image" value="TestImageAdobe"/> </actionGroup> <!-- Go to the category page --> @@ -111,7 +120,33 @@ <!-- Click a swatch and expect to see the configurable product, not see the simple product --> <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '1')}}" stepKey="filterBySwatch1"/> - <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{BaseConfigurableProduct.name}}" stepKey="seeConfigurableProduct"/> + <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="seeConfigurableProduct"/> <dontSee selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeSimpleProduct"/> + + <!-- Assert configurable product in storefront product page --> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Assert configurable product image in storefront product page --> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertProductImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!-- Assert configurable product image in storefront product page --> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertProductSecondImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Click a swatch and expect to see the image from the swatch from the configurable product --> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('adobe-thumb')}}" stepKey="clickSwatchOption"/> + + <!-- Assert swatch option image for configurable product image in storefront product page --> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertSwatchImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml new file mode 100644 index 0000000000000..1ab2cd793f3b8 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSwatchAttributesDisplayInWidgetCMSTest"> + <annotations> + <features value="ConfigurableProduct"/> + <title value="Swatch Attribute is not displayed in the Widget CMS"/> + <description value="Swatch Attribute is not displayed in the Widget CMS"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96469"/> + <useCaseId value="MAGETWO-96406"/> + <group value="ConfigurableProduct"/> + <skip> + <issueId value="MQE-1424" /> + </skip> + </annotations> + + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <createData entity="NewRootCategory" stepKey="createRootCategory"/> + </before> + + <after> + <!--delete created configurable product--> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="visualSwatchAttribute"/> + </actionGroup> + <!--delete root category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageCategoryLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createRootCategory.name$$')}}" stepKey="clickOnDefaultRootCategory"/> + <waitForPageLoad stepKey="waitForPageDefaultCategoryEditLoad" /> + <seeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="assertDeleteButtonIsPresent1"/> + <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="DeleteDefaultRootCategory"/> + <waitForElementVisible selector="{{AdminCategoryModalSection.ok}}" stepKey="waitForModalDeleteDefaultRootCategory" /> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="acceptModal1"/> + <waitForElementVisible selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="waitForPageReloadAfterDeleteDefaultCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <!--logout--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Login--> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <!--Create a configurable swatch product via the UI --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createRootCategory.name$$]" stepKey="searchAndSelectCategory"/> + <!--Add swatch attribute to configurable product--> + <actionGroup ref="AddVisualSwatchToProductWithStorefrontConfigActionGroup" stepKey="addSwatchToProduct"/> + + <!--Create CMS page--> + <actionGroup ref="CreateNewPageWithWidget" stepKey="createCMSPageWithWidget"> + <argument name="category" value="$$createRootCategory.name$$"/> + <argument name="condition" value="Category"/> + <argument name="widgetType" value="Catalog Products List"/> + </actionGroup> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickToExpandSEOSection"/> + <scrollTo selector="{{CmsNewPagePageSeoSection.urlKey}}" stepKey="scrollToUrlKey"/> + <grabValueFrom selector="{{CmsNewPagePageSeoSection.urlKey}}" stepKey="grabTextFromUrlKey"/> + <actionGroup ref="logout" stepKey="logout"/> + + <!--Open Storefront page for the new created page--> + <amOnPage url="{{StorefrontHomePage.url}}$grabTextFromUrlKey" stepKey="gotToCreatedCmsPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productSwatch(visualSwatchOption1.default_label)}}" stepKey="assertAddedWidgetS"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productSwatch(visualSwatchOption2.default_label)}}" stepKey="assertAddedWidgetM"/> + + <!--Login to delete CMS page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="DeletePageByUrlKeyActionGroup" stepKey="deletePage"> + <argument name="UrlKey" value="$grabTextFromUrlKey"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/i18n/de_DE.csv b/app/code/Magento/Swatches/i18n/de_DE.csv deleted file mode 100644 index f0aa34bbef26e..0000000000000 --- a/app/code/Magento/Swatches/i18n/de_DE.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/es_ES.csv b/app/code/Magento/Swatches/i18n/es_ES.csv deleted file mode 100644 index f0aa34bbef26e..0000000000000 --- a/app/code/Magento/Swatches/i18n/es_ES.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/fr_FR.csv b/app/code/Magento/Swatches/i18n/fr_FR.csv deleted file mode 100644 index f0aa34bbef26e..0000000000000 --- a/app/code/Magento/Swatches/i18n/fr_FR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/nl_NL.csv b/app/code/Magento/Swatches/i18n/nl_NL.csv deleted file mode 100644 index f0aa34bbef26e..0000000000000 --- a/app/code/Magento/Swatches/i18n/nl_NL.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/pt_BR.csv b/app/code/Magento/Swatches/i18n/pt_BR.csv deleted file mode 100644 index f0aa34bbef26e..0000000000000 --- a/app/code/Magento/Swatches/i18n/pt_BR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/zh_Hans_CN.csv b/app/code/Magento/Swatches/i18n/zh_Hans_CN.csv deleted file mode 100644 index f0aa34bbef26e..0000000000000 --- a/app/code/Magento/Swatches/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml b/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml index 8d4400b3d0477..e00c41d371c9e 100644 --- a/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml +++ b/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml @@ -21,7 +21,7 @@ $stores = $block->getStoresSortedBySortOrder(); <th class="col-draggable"></th> <th class="col-default"><span><?= $block->escapeHtml(__('Is Default')) ?></span></th> <?php foreach ($stores as $_store): ?> - <th class="col-swatch col-<%- data.id %> + <th class="col-swatch col-swatch-min-width col-<%- data.id %> <?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> _required<?php endif; ?>" colspan="2"> <span><?= $block->escapeHtml($_store->getName()) ?></span> @@ -75,7 +75,7 @@ $stores = $block->getStoresSortedBySortOrder(); </td> <?php foreach ($stores as $_store): ?> <?php $storeId = (int)$_store->getId(); ?> - <td class="col-swatch col-<%- data.id %>"> + <td class="col-swatch col-swatch-min-width col-<%- data.id %>"> <input class="input-text swatch-text-field-<?= /* @noEscape */ $storeId ?> <?php if ($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option required-unique<?php endif; ?>" @@ -83,7 +83,7 @@ $stores = $block->getStoresSortedBySortOrder(); type="text" value="<%- data.swatch<?= /* @noEscape */ $storeId ?> %>" placeholder="<?= $block->escapeHtml(__("Swatch")) ?>"/> </td> - <td class="swatch-col-<%- data.id %>"> + <td class="col-swatch-min-width swatch-col-<%- data.id %>"> <input name="optiontext[value][<%- data.id %>][<?= /* @noEscape */ $storeId ?>]" value="<%- data.store<?= /* @noEscape */ $storeId ?> %>" class="input-text<?php if ($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option<?php endif; ?>" diff --git a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css index d170ed0345a03..b0ea10b1ed968 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css @@ -149,6 +149,26 @@ width: 50px; } +.col-swatch-min-width { + min-width: 65px; +} + +[class^=swatch-col], +[class^=col-]:not(.col-draggable):not(.col-default) { + min-width: 150px; +} + +#swatch-visual-options-panel, +#swatch-text-options-panel, +#manage-options-panel { + overflow: auto; + width: 100%; +} + +.data-table .col-swatch-min-width input[type="text"] { + padding: inherit; +} + .swatches-visual-col.unavailable:after { content: ''; position: absolute; diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 0000000000000..91798cbd9947f --- /dev/null +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,12 @@ +<!-- + ~ Copyright © Magento, Inc. All rights reserved. + ~ See COPYING.txt for license details. + --> + +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="category.product.type.widget.details.renderers"> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + </referenceBlock> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml index 3492f83fd1828..d817000f7bc46 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml @@ -17,7 +17,7 @@ <a href="<?= /* @escapeNotVerified */ $label['link'] ?>" aria-label="<?= /* @escapeNotVerified */ $label['label'] ?>" class="swatch-option-link-layered"> - <?php if (isset($swatchData['swatches'][$option]['type'])) { ?> + <?php if (isset($swatchData['swatches'][$option]['type'])): ?> <?php switch ($swatchData['swatches'][$option]['type']) { case '3': ?> @@ -32,10 +32,8 @@ <?php break; case '2': ?> - <?php $swatchThumbPath = $block->getSwatchPath('swatch_thumb', - $swatchData['swatches'][$option]['value']); ?> - <?php $swatchImagePath = $block->getSwatchPath('swatch_image', - $swatchData['swatches'][$option]['value']); ?> + <?php $swatchThumbPath = $block->getSwatchPath('swatch_thumb', $swatchData['swatches'][$option]['value']); ?> + <?php $swatchImagePath = $block->getSwatchPath('swatch_image', $swatchData['swatches'][$option]['value']); ?> <div class="swatch-option image <?= /* @escapeNotVerified */ $label['custom_style'] ?>" tabindex="-1" option-type="2" @@ -69,7 +67,7 @@ ><?= /* @escapeNotVerified */ $swatchData['swatches'][$option]['value'] ?></div> <?php break; } ?> - <?php } ?> + <?php endif; ?> </a> <?php endforeach; ?> </div> diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 938028f62502d..2571c0385dab7 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -406,7 +406,7 @@ define([ if ($widget.options.enableControlLabel) { label += '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' + - item.label + + $('<i></i>').text(item.label).html() + '</span>' + '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>'; } @@ -414,7 +414,7 @@ define([ if ($widget.inProductList) { $widget.productForm.append(input); input = ''; - listLabel = 'aria-label="' + item.label + '"'; + listLabel = 'aria-label="' + $('<i></i>').text(item.label).html() + '"'; } else { listLabel = 'aria-labelledby="' + controlLabelId + '"'; } @@ -493,7 +493,7 @@ define([ return ''; } - $.each(config.options, function () { + $.each(config.options, function (index) { var id, type, value, @@ -511,18 +511,20 @@ define([ // Add more button if (moreLimit === countAttributes++) { - html += '<a href="#" class="' + moreClass + '">' + moreText + '</a>'; + html += '<a href="#" class="' + moreClass + '"><span>' + moreText + '</span></a>'; } id = this.id; type = parseInt(optionConfig[id].type, 10); - value = optionConfig[id].hasOwnProperty('value') ? optionConfig[id].value : ''; + value = optionConfig[id].hasOwnProperty('value') ? + $('<i></i>').text(optionConfig[id].value).html() : ''; thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : ''; width = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.width : 110; height = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.height : 90; - label = this.label ? this.label : ''; + label = this.label ? $('<i></i>').text(this.label).html() : ''; attr = ' id="' + controlId + '-item-' + id + '"' + + ' index="' + index + '"' + ' aria-checked="false"' + ' aria-describedby="' + controlId + '"' + ' tabindex="0"' + @@ -745,6 +747,12 @@ define([ $widget._UpdatePrice(); } + $(document).trigger('updateMsrpPriceBlock', + [ + parseInt($this.attr('index'), 10) + 1, + $widget.options.jsonConfig.optionPrices + ]); + $widget._loadMedia(); $input.trigger('change'); }, @@ -917,7 +925,8 @@ define([ $productPrice = $product.find(this.options.selectorProductPrice), options = _.object(_.keys($widget.optionsMap), {}), result, - tierPriceHtml; + tierPriceHtml, + isShow; $widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function () { var attributeId = $(this).attr('attribute-id'); @@ -934,11 +943,9 @@ define([ } ); - if (typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount) { - $(this.options.slyOldPriceSelector).show(); - } else { - $(this.options.slyOldPriceSelector).hide(); - } + isShow = typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount; + + $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide'](); if (typeof result != 'undefined' && result.tierPrices.length) { if (this.options.tierPriceTemplate) { @@ -1029,14 +1036,10 @@ define([ _.each(allowedProducts, function (allowedProduct) { optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); - if (_.isEmpty(product)) { + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { optionMinPrice = optionFinalPrice; product = allowedProduct; } - - if (optionFinalPrice < optionMinPrice) { - product = allowedProduct; - } }, this); return product; @@ -1220,8 +1223,8 @@ define([ updateBaseImage: function (images, context, isInProductView) { var justAnImage = images[0], initialImages = this.options.mediaGalleryInitial, - gallery = context.find(this.options.mediaGallerySelector).data('gallery'), imagesToUpdate, + gallery = context.find(this.options.mediaGallerySelector).data('gallery'), isInitial; if (isInProductView) { @@ -1233,7 +1236,15 @@ define([ } imagesToUpdate = this._setImageIndex(imagesToUpdate); - gallery.updateData(imagesToUpdate); + + if (!_.isUndefined(gallery)) { + gallery.updateData(imagesToUpdate); + } else { + context.find(this.options.mediaGallerySelector).on('gallery:loaded', function (loadedGallery) { + loadedGallery = context.find(this.options.mediaGallerySelector).data('gallery'); + loadedGallery.updateData(imagesToUpdate); + }.bind(this)); + } if (isInitial) { $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); @@ -1243,6 +1254,7 @@ define([ dataMergeStrategy: this.options.gallerySwitchStrategy }); } + } else if (justAnImage && justAnImage.img) { context.find('.product-image-photo').attr('src', justAnImage.img); } diff --git a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php index bad64260cf58a..939facd02c02d 100644 --- a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php +++ b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php @@ -7,6 +7,9 @@ use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +/** + * Abstract aggregate calculator. + */ abstract class AbstractAggregateCalculator extends AbstractCalculator { /** @@ -106,11 +109,12 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ $rowTaxes = []; $rowTaxesBeforeDiscount = []; $appliedTaxes = []; + $rowTotalForTaxCalculation = $this->getPriceForTaxCalculation($item, $price) * $quantity; //Apply each tax rate separately foreach ($appliedRates as $appliedRate) { $taxId = $appliedRate['id']; $taxRate = $appliedRate['percent']; - $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotal, $taxRate, false, false); + $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotalForTaxCalculation, $taxRate, false, false); $deltaRoundingType = self::KEY_REGULAR_DELTA_ROUNDING; if ($applyTaxAfterDiscount) { $deltaRoundingType = self::KEY_TAX_BEFORE_DISCOUNT_DELTA_ROUNDING; @@ -121,7 +125,10 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ //Handle discount if ($applyTaxAfterDiscount) { //TODO: handle originalDiscountAmount - $taxableAmount = max($rowTotal - $discountAmount, 0); + $taxableAmount = max($rowTotalForTaxCalculation - $discountAmount, 0); + if ($taxableAmount && !$applyTaxAfterDiscount) { + $taxableAmount = $rowTotalForTaxCalculation; + } $rowTaxAfterDiscount = $this->calculationTool->calcTaxAmount( $taxableAmount, $taxRate, @@ -168,6 +175,26 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ ->setAppliedTaxes($appliedTaxes); } + /** + * Get price for tax calculation. + * + * @param QuoteDetailsItemInterface $item + * @param float $price + * @return float + */ + private function getPriceForTaxCalculation(QuoteDetailsItemInterface $item, float $price) + { + if ($item->getExtensionAttributes() && $item->getExtensionAttributes()->getPriceForTaxCalculation()) { + $priceForTaxCalculation = $this->calculationTool->round( + $item->getExtensionAttributes()->getPriceForTaxCalculation() + ); + } else { + $priceForTaxCalculation = $price; + } + + return $priceForTaxCalculation; + } + /** * Round amount * diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index 0901e1b7bc78c..77b3cfa3a08bb 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -15,12 +15,17 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; use Magento\Tax\Api\Data\TaxClassKeyInterface; use Magento\Tax\Api\Data\TaxDetailsInterface; use Magento\Tax\Api\Data\TaxDetailsItemInterface; use Magento\Tax\Api\Data\QuoteDetailsInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterfaceFactory; /** * Tax totals calculation model @@ -129,6 +134,16 @@ class CommonTaxCollector extends AbstractTotal */ protected $quoteDetailsItemDataObjectFactory; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var QuoteDetailsItemExtensionInterfaceFactory + */ + private $quoteDetailsItemExtensionFactory; + /** * Class constructor * @@ -139,6 +154,8 @@ class CommonTaxCollector extends AbstractTotal * @param \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory * @param CustomerAddressFactory $customerAddressFactory * @param CustomerAddressRegionFactory $customerAddressRegionFactory + * @param TaxHelper|null $taxHelper + * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory */ public function __construct( \Magento\Tax\Model\Config $taxConfig, @@ -147,7 +164,9 @@ public function __construct( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory, \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory, CustomerAddressFactory $customerAddressFactory, - CustomerAddressRegionFactory $customerAddressRegionFactory + CustomerAddressRegionFactory $customerAddressRegionFactory, + TaxHelper $taxHelper = null, + QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null ) { $this->taxCalculationService = $taxCalculationService; $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory; @@ -156,6 +175,9 @@ public function __construct( $this->quoteDetailsItemDataObjectFactory = $quoteDetailsItemDataObjectFactory; $this->customerAddressFactory = $customerAddressFactory; $this->customerAddressRegionFactory = $customerAddressRegionFactory; + $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class); + $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?: + ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class); } /** @@ -186,7 +208,7 @@ public function mapAddress(QuoteAddress $address) * @param bool $priceIncludesTax * @param bool $useBaseCurrency * @param string $parentCode - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function mapItem( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -199,7 +221,7 @@ public function mapItem( $sequence = 'sequence-' . $this->getNextIncrement(); $item->setTaxCalculationItemId($sequence); } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($item->getTaxCalculationItemId()) ->setQuantity($item->getQty()) @@ -215,12 +237,28 @@ public function mapItem( if (!$item->getBaseTaxCalculationPrice()) { $item->setBaseTaxCalculationPrice($item->getBaseCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $baseTaxCalculationPrice = $item->getBaseOriginalPrice(); + } else { + $baseTaxCalculationPrice = $item->getBaseCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$baseTaxCalculationPrice); + $itemDataObject->setUnitPrice($item->getBaseTaxCalculationPrice()) ->setDiscountAmount($item->getBaseDiscountAmount()); } else { if (!$item->getTaxCalculationPrice()) { $item->setTaxCalculationPrice($item->getCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $taxCalculationPrice = $item->getOriginalPrice(); + } else { + $taxCalculationPrice = $item->getCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$taxCalculationPrice); + $itemDataObject->setUnitPrice($item->getTaxCalculationPrice()) ->setDiscountAmount($item->getDiscountAmount()); } @@ -230,6 +268,23 @@ public function mapItem( return $itemDataObject; } + /** + * Set price for tax calculation. + * + * @param QuoteDetailsItemInterface $quoteDetailsItem + * @param float $taxCalculationPrice + * @return void + */ + private function setPriceForTaxCalculation(QuoteDetailsItemInterface $quoteDetailsItem, float $taxCalculationPrice) + { + $extensionAttributes = $quoteDetailsItem->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->quoteDetailsItemExtensionFactory->create(); + } + $extensionAttributes->setPriceForTaxCalculation($taxCalculationPrice); + $quoteDetailsItem->setExtensionAttributes($extensionAttributes); + } + /** * Map item extra taxables * @@ -237,7 +292,7 @@ public function mapItem( * @param AbstractItem $item * @param bool $priceIncludesTax * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @return QuoteDetailsItemInterface[] */ public function mapItemExtraTaxables( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -260,7 +315,7 @@ public function mapItemExtraTaxables( } else { $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE]; } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_CODE]) ->setType($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TYPE]) @@ -283,9 +338,9 @@ public function mapItemExtraTaxables( * Add quote items * * @param ShippingAssignmentInterface $shippingAssignment - * @param bool $useBaseCurrency * @param bool $priceIncludesTax - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @param bool $useBaseCurrency + * @return QuoteDetailsItemInterface[] */ public function mapItems( ShippingAssignmentInterface $shippingAssignment, @@ -293,7 +348,7 @@ public function mapItems( $useBaseCurrency ) { $items = $shippingAssignment->getItems(); - if (!count($items)) { + if (empty($items)) { return []; } @@ -361,10 +416,12 @@ public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAd } /** + * Get shipping data object. + * * @param ShippingAssignmentInterface $shippingAssignment * @param QuoteAddress\Total $total * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function getShippingDataObject( ShippingAssignmentInterface $shippingAssignment, @@ -379,7 +436,7 @@ public function getShippingDataObject( $total->setBaseShippingTaxCalculationAmount($total->getBaseShippingAmount()); } if ($total->getShippingTaxCalculationAmount() !== null) { - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $this->quoteDetailsItemDataObjectFactory->create() ->setType(self::ITEM_TYPE_SHIPPING) ->setCode(self::ITEM_CODE_SHIPPING) @@ -414,14 +471,14 @@ public function getShippingDataObject( * Populate QuoteDetails object from quote address object * * @param ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] $itemDataObjects + * @param QuoteDetailsItemInterface[] $itemDataObjects * @return \Magento\Tax\Api\Data\QuoteDetailsInterface */ protected function prepareQuoteDetails(ShippingAssignmentInterface $shippingAssignment, $itemDataObjects) { $items = $shippingAssignment->getItems(); $address = $shippingAssignment->getShipping()->getAddress(); - if (!count($items)) { + if (empty($items)) { return $this->quoteDetailsDataObjectFactory->create(); } @@ -543,6 +600,7 @@ protected function processProductItems( * Process applied taxes for items and quote * * @param QuoteAddress\Total $total + * @param ShippingAssignmentInterface $shippingAssignment * @param array $itemsByType * @return $this */ @@ -630,6 +688,9 @@ public function updateItemTaxInfo($quoteItem, $itemTaxDetails, $baseItemTaxDetai { //The price should be base price $quoteItem->setPrice($baseItemTaxDetails->getPrice()); + if ($quoteItem->getCustomPrice() && $this->taxHelper->applyTaxOnCustomPrice()) { + $quoteItem->setCustomPrice($baseItemTaxDetails->getPrice()); + } $quoteItem->setConvertedPrice($itemTaxDetails->getPrice()); $quoteItem->setPriceInclTax($itemTaxDetails->getPriceInclTax()); $quoteItem->setRowTotal($itemTaxDetails->getRowTotal()); @@ -846,8 +907,9 @@ protected function saveAppliedTaxes() } /** - * Increment and return counter. This function is intended to be used to generate temporary - * id for an item. + * Increment and return counter. + * + * This function is intended to be used to generate temporary id for an item. * * @return int */ diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php index 4aea7ab4c5a7c..52061fd5d3882 100755 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php @@ -265,7 +265,7 @@ protected function processExtraTaxables(Address\Total $total, array $itemsByType { $extraTaxableDetails = []; foreach ($itemsByType as $itemType => $itemTaxDetails) { - if ($itemType != self::ITEM_TYPE_PRODUCT and $itemType != self::ITEM_TYPE_SHIPPING) { + if ($itemType != self::ITEM_TYPE_PRODUCT && $itemType != self::ITEM_TYPE_SHIPPING) { foreach ($itemTaxDetails as $itemCode => $itemTaxDetail) { /** @var \Magento\Tax\Api\Data\TaxDetailsInterface $taxDetails */ $taxDetails = $itemTaxDetail[self::KEY_ITEM]; @@ -408,6 +408,7 @@ protected function enhanceTotalData( /** * Process model configuration array. + * * This method can be used for changing totals collect sort order * * @param array $config diff --git a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php index 87f65ef311ac2..208833733ae3f 100644 --- a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php @@ -6,6 +6,10 @@ namespace Magento\Tax\Plugin\Checkout\CustomerData; +/** + * Process quote items price, considering tax configuration. + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class Cart { /** @@ -68,6 +72,16 @@ public function afterGetSectionData(\Magento\Checkout\CustomerData\Cart $subject $this->itemPriceRenderer->setItem($item); $this->itemPriceRenderer->setTemplate('checkout/cart/item/price/sidebar.phtml'); $result['items'][$key]['product_price']=$this->itemPriceRenderer->toHtml(); + if ($this->itemPriceRenderer->displayPriceExclTax()) { + $result['items'][$key]['product_price_value'] = $item->getCalculationPrice(); + } elseif ($this->itemPriceRenderer->displayPriceInclTax()) { + $result['items'][$key]['product_price_value'] = $item->getPriceInclTax(); + } elseif ($this->itemPriceRenderer->displayBothPrices()) { + //unset product price value in case price already has been set as scalar value. + unset($result['items'][$key]['product_price_value']); + $result['items'][$key]['product_price_value']['incl_tax'] = $item->getPriceInclTax(); + $result['items'][$key]['product_price_value']['excl_tax'] = $item->getCalculationPrice(); + } } } } diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml index 596b72070bf7e..3986ede9acf63 100644 --- a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml @@ -128,12 +128,12 @@ <!--Select Configuration menu from Store--> <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES" /> <waitForPageLoad stepKey="waitForConfiguration"/> - <click selector="{{StoresSubmenuSection.configuration}}" stepKey="clickOnConfigurations"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations"/> <waitForPageLoad stepKey="waitForSales"/> <!--Double click the same to fix flaky issue with redirection to Dashboard--> <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES1" /> <waitForPageLoad stepKey="waitForConfiguration1"/> - <click selector="{{StoresSubmenuSection.configuration}}" stepKey="clickOnConfigurations1"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations1"/> <waitForPageLoad stepKey="waitForSales1" time="5"/> <!--Change default tax class for Shipping on Taxable Goods--> <click selector="{{ConfigurationListSection.sales}}" stepKey="clickOnSales" /> @@ -156,12 +156,12 @@ <!--Select Configuration menu from Store--> <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES" /> <waitForPageLoad stepKey="waitForConfiguration"/> - <click selector="{{StoresSubmenuSection.configuration}}" stepKey="clickOnConfigurations"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations"/> <waitForPageLoad stepKey="waitForSales"/> <!--Double click the same to fix flaky issue with redirection to Dashboard--> <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES1" /> <waitForPageLoad stepKey="waitForConfiguration1"/> - <click selector="{{StoresSubmenuSection.configuration}}" stepKey="clickOnConfigurations1"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations1"/> <waitForPageLoad stepKey="waitForSales1"/> <!--Change default tax class for Shipping on Taxable Goods--> <click selector="{{ConfigurationListSection.sales}}" stepKey="clickOnSales" /> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Tax/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..2bed9b0d07918 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,26 @@ +<?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="AdminMenuStoresTaxesTaxRules"> + <data key="pageTitle">Tax Rules</data> + <data key="title">Tax Rules</data> + <data key="dataUiId">magento-tax-sales-tax-rules</data> + </entity> + <entity name="AdminMenuStoresTaxZonesAndRates"> + <data key="pageTitle">Tax Zones and Rates</data> + <data key="title">Tax Zones and Rates</data> + <data key="dataUiId">magento-tax-sales-tax-rates</data> + </entity> + <entity name="AdminMenuSystemDataTransferImportAndExportTaxRates"> + <data key="pageTitle">Import and Export Tax Rates</data> + <data key="title">Import/Export Tax Rates</data> + <data key="dataUiId">magento-taximportexport-system-convert-tax</data> + </entity> +</entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml index 887203a76fdad..4409ea0a21df6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml @@ -106,4 +106,8 @@ <data key="zip_is_range">0</data> <data key="rate">0.1</data> </entity> + <entity name="taxRateForPensylvannia" extends="defaultTaxRate"> + <data key="tax_region_id">51</data> + <data key="rate">6</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml new file mode 100644 index 0000000000000..26152d5497a98 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditTaxRatePage" url="tax/rate/edit/rate/{{var1}}/" module="Magento_Tax" area="admin" parameterized="true"> + <section name="AdminTaxRateFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml new file mode 100644 index 0000000000000..c0e4958619c89 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditTaxRulePage" url="tax/rule/edit/rule/{{var}}/" module="Magento_Tax" area="admin" parameterized="true"> + <section name="AdminTaxRulesSection"/> + </page> +</pages> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml index bfb082c145f07..e69bfbaebbfd9 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml @@ -28,6 +28,8 @@ <element name="taxCalculationPrices" type="select" selector="#tax_calculation_price_includes_tax"/> <element name="taxCalculationPricesDisabled" type="select" selector="#tax_calculation_price_includes_tax[disabled='disabled']"/> <element name="taxCalculationPricesInherit" type="checkbox" selector="#tax_calculation_price_includes_tax_inherit"/> + <element name="taxCalculationApplyTaxOn" type="select" selector="#tax_calculation_apply_tax_on"/> + <element name="taxCalculationApplyTaxOnInherit" type="checkbox" selector="#tax_calculation_apply_tax_on_inherit"/> <element name="defaultDestination" type="block" selector="#tax_defaults-head" timeout="30"/> <element name="systemValueDefaultState" type="checkbox" selector="#row_tax_defaults_region input[type='checkbox']"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml index 29c53242b90f6..46d92e30395e0 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml @@ -33,5 +33,6 @@ <element name="deleteTaxClass" type="button" selector="//span[contains(text(),'{{var1}}')]/../..//*[@class='mselect-delete']" parameterized="true"/> <element name="popUpDialogOK" type="button" selector="//*[@class='modal-footer']//*[contains(text(),'OK')]"/> <element name="taxRateMultiSelectItems" type="block" selector=".mselect-list-item"/> + <element name="taxRateNumber" type="button" selector="//div[@data-ui-id='tax-rate-form-fieldset-element-form-field-tax-rate']//div[@class='mselect-items-wrapper']//label[{{var}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml index 6d9717318efc8..f75fa716e9d30 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml @@ -11,7 +11,7 @@ <test name="AdminCreateTaxRateZipCodeRangeTest"> <annotations> <stories value="Create tax rate"/> - <title value="Create tax tate, zip code range"/> + <title value="Create tax rate, zip code range"/> <description value="Test log in to Create Tax Rate and Create Zip Code Range"/> <testCaseId value="MC-5319"/> <severity value="CRITICAL"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml new file mode 100644 index 0000000000000..1277d6e5f9fd2 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresTaxRulesNavigateMenuTest"> + <annotations> + <features value="Tax"/> + <stories value="Menu Navigation"/> + <title value="Admin stores tax rules navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Tax Rules"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14175"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTaxRulesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresTaxesTaxRules.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresTaxesTaxRules.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml new file mode 100644 index 0000000000000..e0a4d5d9a4016 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminStoresTaxZonesAndRatesNavigateMenuTest"> + <annotations> + <features value="Tax"/> + <stories value="Menu Navigation"/> + <title value="Admin stores tax zones and rates navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Tax Zones and Rates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14176"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTaxZonesAndRatesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresTaxZonesAndRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresTaxZonesAndRates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml new file mode 100644 index 0000000000000..a84ae61d66305 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemImportExportTaxRatesNavigateMenuTest"> + <annotations> + <features value="Tax"/> + <stories value="Menu Navigation"/> + <title value="Admin system import export tax rates navigate menu test"/> + <description value="Admin should be able to navigate to System > Import/Export Tax Rates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14177"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemImportExportTaxRatesPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemDataTransferImportAndExportTaxRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemDataTransferImportAndExportTaxRates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml new file mode 100644 index 0000000000000..732470d2558c7 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml @@ -0,0 +1,79 @@ +<?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="AdminTaxCalcWithApplyTaxOnSettingTest"> + <annotations> + <features value="AdminTaxCalcWithApplyTaxOnSettingTest"/> + <title value="Tax calculation process following 'Apply Tax On' setting"/> + <description value="Tax calculation process following 'Apply Tax On' setting"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11026"/> + <useCaseId value="MC-4316"/> + <group value="Tax"/> + </annotations> + + <before> + <createData entity="taxRateForPensylvannia" stepKey="initialTaxRate"/> + <createData entity="defaultTaxRule" stepKey="createTaxRule"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="SetTaxClassForShipping" stepKey="setTaxClass"/> + <actionGroup ref="SetTaxApplyOnSetting" stepKey="setApplyTaxOnSetting"> + <argument name="userInput" value="Original price only"/> + </actionGroup> + <amOnPage url="{{AdminEditTaxRulePage.url($$createTaxRule.id$$)}}" stepKey="navigateToEditTaxRulePage"/> + <waitForPageLoad stepKey="waitEditTaxRulePageToLoad"/> + <click selector="{{AdminTaxRulesSection.taxRateNumber('1')}}" stepKey="clickonTaxRate"/> + <click selector="{{AdminTaxRulesSection.deleteTaxClassName($$initialTaxRate.code$$)}}" stepKey="checkTaxRate"/> + <click selector="{{AdminTaxRulesSection.saveRule}}" stepKey="saveChanges"/> + <waitForPageLoad stepKey="waitTaxRulesToBeSaved"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rule." stepKey="seeSuccessMessage2"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + <actionGroup ref="DisableTaxApplyOnOriginalPrice" stepKey="setApplyTaxOffSetting"> + <argument name="userInput" value="Custom price if available"/> + </actionGroup> + <actionGroup ref="ResetTaxClassForShipping" stepKey="resetTaxClassForShipping"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="gotoNewOrderCreationPage"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createProduct$$"></argument> + </actionGroup> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillEmailField"/> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <scrollTo selector="{{AdminOrderFormAccountSection.email}}" stepKey="scrollToEmailField"/> + <waitForElementVisible selector="{{AdminOrderFormAccountSection.email}}" stepKey="waitEmailFieldToBeVisible"/> + <click selector="{{AdminOrderFormShippingAddressSection.SameAsBilling}}" stepKey="uncheckSameAsBillingAddressCheckbox"/> + <waitForPageLoad stepKey="waitSectionToReload"/> + <selectOption selector="{{AdminOrderFormShippingAddressSection.State}}" stepKey="switchOnVisibleInAdvancedSearch" userInput="Pennsylvania"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="getShippingMethods"/> + <waitForPageLoad stepKey="waitForApplyingShippingMethods"/> + <grabTextFrom selector="{{AdminOrderFormTotalSection.subtotalRow('3')}}" stepKey="grabTaxCost"/> + <assertEquals expected='$6.00' expectedType="string" actual="($grabTaxCost)" stepKey="assertTax"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="scrollToSubmitButton"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitElementToBeVisble"/> + <click selector="{{AdminOrderFormItemsSection.customPriceCheckbox}}" stepKey="clickOnCustomPriceCheckbox"/> + <fillField selector="{{AdminOrderFormItemsSection.customPriceField}}" userInput="{{SimpleProductNameWithDoubleQuote.price}}" stepKey="changePrice"/> + <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="updateItemsAndQunatities"/> + <assertEquals expected='$6.00' expectedType="string" actual="($grabTaxCost)" stepKey="assertTaxAfterCustomPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml index 4741898b0ab86..628d189823a52 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml @@ -136,9 +136,8 @@ <waitForPageLoad stepKey="waitForInvoicePageOpened"/> <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForInvoiceSaved"/> - <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction" after="waitForInvoiceSaved"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl" after="clickShipAction"/> <!--Submit Shipment--> <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment" after="seeOrderShipmentUrl"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml index 3ef279e4a76a2..a96a57cbfec55 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml @@ -95,7 +95,11 @@ <argument name="address" value="US_Address_Utah" /> </actionGroup> <scrollTo selector="{{StorefrontProductPageSection.orderTotal}}" x="0" y="-80" stepKey="scrollToOrderTotal"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.shipping}}" time="30" stepKey="waitForShipping"/> + <see selector="{{StorefrontProductPageSection.shipping}}" userInput="$5.00" stepKey="seeShipping"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.tax}}" time="30" stepKey="waitForTax"/> <see selector="{{StorefrontProductPageSection.tax}}" userInput="$20.00" stepKey="seeAssertTaxAmount" /> + <waitForElementVisible selector="{{StorefrontProductPageSection.orderTotal}}" time="30" stepKey="waitForOrderTotal"/> <see selector="{{StorefrontProductPageSection.orderTotal}}" userInput="$125.00" stepKey="seeAssertGrandTotal"/> </test> </tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index 1cb96b37cc760..aa44593400a89 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -89,8 +89,8 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> - <see selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country_id}}" stepKey="checkCustomerCountry" /> - <see selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml index 190263efb2469..ac090fd4fe9c0 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml @@ -61,8 +61,8 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> - <see selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_NY.country_id}}" stepKey="checkCustomerCountry" /> - <see selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_NY.state}}" stepKey="checkCustomerRegion" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_NY.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_NY.state}}" stepKey="checkCustomerRegion" /> <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> <expectedResult type="string">{{US_Address_NY.postcode}}</expectedResult> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml index 433e390d776de..2ed31c2e20488 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml @@ -37,6 +37,7 @@ <!-- Update 0.1 tax rate on the tax rate form page --> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateFrance.code}}" stepKey="fillTaxIdentifierField2"/> <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateFrance.tax_country_id}}" stepKey="selectCountry1"/> + <wait time="10" stepKey="waitForRegionsLoaded" /> <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateFrance.tax_region_id}}" stepKey="selectState"/> <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateFrance.tax_postcode}}" stepKey="fillPostCode"/> <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateFrance.rate}}" stepKey="fillRate1"/> diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php index bf49f3d479132..77da6950fecf7 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php @@ -252,7 +252,7 @@ private function getTaxRateMock(array $taxRateData) foreach ($taxRateData as $key => $value) { // convert key from snake case to upper case $taxRateMock->expects($this->any()) - ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) + ->method('get' . str_replace('_', '', ucwords($key, '_'))) ->will($this->returnValue($value)); } diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php index cbd7ed46e38d7..2a7eeb27ee07e 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php @@ -9,6 +9,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Tax\Model\Calculation\RowBaseCalculator; use Magento\Tax\Model\Calculation\TotalBaseCalculator; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -66,6 +67,11 @@ class RowBaseAndTotalBaseCalculatorTestCase extends \PHPUnit\Framework\TestCase */ protected $taxDetailsItem; + /** + * @var \Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteDetailsItemExtension; + /** * initialize all mocks * @@ -101,7 +107,14 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class)->getMock(); + $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class) + ->disableOriginalConstructor()->setMethods(['getExtensionAttributes', 'getUnitPrice']) + ->getMockForAbstractClass(); + $this->quoteDetailsItemExtension = $this->getMockBuilder(QuoteDetailsItemExtensionInterface::class) + ->disableOriginalConstructor()->setMethods(['getPriceForTaxCalculation']) + ->getMockForAbstractClass(); + $this->mockItem->expects($this->any())->method('getExtensionAttributes') + ->willReturn($this->quoteDetailsItemExtension); $this->appliedTaxDataObjectFactory = $this->createPartialMock( \Magento\Tax\Api\Data\AppliedTaxInterfaceFactory::class, diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php index 9325ec10dc627..50d45ad662bd4 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -3,79 +3,106 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Tax\Test\Unit\Model\Sales\Total\Quote; -/** - * Test class for \Magento\Tax\Model\Sales\Total\Quote\Tax - */ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Tax\Api\Data\TaxDetailsItemInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Store\Model\Store; +use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; +use Magento\Tax\Model\Config; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Model\Sales\Quote\ItemDetails; +use Magento\Tax\Model\TaxClass\Key as TaxClassKey; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Api\Data\ShippingInterface; +use Magento\Quote\Model\Quote\Address\Total as QuoteAddressTotal; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** + * Common tax collector test + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase +class CommonTaxCollectorTest extends TestCase { /** - * @var \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector + * @var CommonTaxCollector */ private $commonTaxCollector; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Tax\Model\Config + * @var MockObject|Config */ private $taxConfig; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\Quote\Address + * @var MockObject|QuoteAddress */ private $address; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\Quote + * @var MockObject|Quote */ private $quote; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\Store + * @var MockObject|Store */ private $store; /** - * @var \PHPUnit_Framework_MockObject_MockObject| + * @var MockObject */ protected $taxClassKeyDataObjectFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject| + * @var MockObject */ protected $quoteDetailsItemDataObjectFactoryMock; /** - * @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @var QuoteDetailsItemInterface */ protected $quoteDetailsItemDataObject; /** - * @var \Magento\Tax\Api\Data\TaxClassKeyInterface + * @var TaxClassKeyInterface */ protected $taxClassKeyDataObject; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * {@inheritdoc} + */ protected function setUp() { $objectManager = new ObjectManager($this); - $this->taxConfig = $this->getMockBuilder(\Magento\Tax\Model\Config::class) + $this->taxConfig = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() - ->setMethods(['getShippingTaxClass', 'shippingPriceIncludesTax']) + ->setMethods(['getShippingTaxClass', 'shippingPriceIncludesTax', 'discountTax']) ->getMock(); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $this->store = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods(['__wakeup']) ->getMock(); - $this->quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $this->quote = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['__wakeup', 'getStore']) ->getMock(); @@ -84,7 +111,7 @@ protected function setUp() ->method('getStore') ->will($this->returnValue($this->store)); - $this->address = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) + $this->address = $this->getMockBuilder(QuoteAddress::class) ->disableOriginalConstructor() ->getMock(); @@ -92,35 +119,41 @@ protected function setUp() ->method('getQuote') ->will($this->returnValue($this->quote)); $methods = ['create']; - $this->quoteDetailsItemDataObject = $objectManager->getObject( - \Magento\Tax\Model\Sales\Quote\ItemDetails::class - ); - $this->taxClassKeyDataObject = $objectManager->getObject(\Magento\Tax\Model\TaxClass\Key::class); + $this->quoteDetailsItemDataObject = $objectManager->getObject(ItemDetails::class); + $this->taxClassKeyDataObject = $objectManager->getObject(TaxClassKey::class); $this->quoteDetailsItemDataObjectFactoryMock - = $this->createPartialMock(\Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory::class, $methods); + = $this->createPartialMock(QuoteDetailsItemInterfaceFactory::class, $methods); $this->quoteDetailsItemDataObjectFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->quoteDetailsItemDataObject); $this->taxClassKeyDataObjectFactoryMock = - $this->createPartialMock(\Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory::class, $methods); + $this->createPartialMock(TaxClassKeyInterfaceFactory::class, $methods); $this->taxClassKeyDataObjectFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->taxClassKeyDataObject); + $this->taxHelper = $this->getMockBuilder(TaxHelper::class) + ->disableOriginalConstructor() + ->getMock(); $this->commonTaxCollector = $objectManager->getObject( - \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector::class, + CommonTaxCollector::class, [ 'taxConfig' => $this->taxConfig, 'quoteDetailsItemDataObjectFactory' => $this->quoteDetailsItemDataObjectFactoryMock, - 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock + 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock, + 'taxHelper' => $this->taxHelper, ] ); } /** + * Test for GetShippingDataObject + * * @param array $addressData * @param bool $useBaseCurrency * @param string $shippingTaxClass * @param bool $shippingPriceInclTax + * + * @return void * @dataProvider getShippingDataObjectDataProvider */ public function testGetShippingDataObject( @@ -128,8 +161,8 @@ public function testGetShippingDataObject( $useBaseCurrency, $shippingTaxClass, $shippingPriceInclTax - ) { - $shippingAssignmentMock = $this->createMock(\Magento\Quote\Api\Data\ShippingAssignmentInterface::class); + ): void { + $shippingAssignmentMock = $this->createMock(ShippingAssignmentInterface::class); $methods = [ 'getShippingDiscountAmount', 'getShippingTaxCalculationAmount', @@ -139,8 +172,10 @@ public function testGetShippingDataObject( 'getBaseShippingAmount', 'getBaseShippingDiscountAmount' ]; - $totalsMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Total::class, $methods); - $shippingMock = $this->createMock(\Magento\Quote\Api\Data\ShippingInterface::class); + /** @var MockObject|QuoteAddressTotal $totalsMock */ + $totalsMock = $this->createPartialMock(QuoteAddressTotal::class, $methods); + $shippingMock = $this->createMock(ShippingInterface::class); + /** @var MockObject|ShippingAssignmentInterface $shippingAssignmentMock */ $shippingAssignmentMock->expects($this->once())->method('getShipping')->willReturn($shippingMock); $shippingMock->expects($this->once())->method('getAddress')->willReturn($this->address); $baseShippingAmount = $addressData['base_shipping_amount']; @@ -184,9 +219,44 @@ public function testGetShippingDataObject( } /** + * Update item tax info + * + * @return void + */ + public function testUpdateItemTaxInfo(): void + { + /** @var MockObject|QuoteItem $quoteItem */ + $quoteItem = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getPrice', 'setPrice', 'getCustomPrice', 'setCustomPrice']) + ->getMock(); + $this->taxHelper->method('applyTaxOnCustomPrice')->willReturn(true); + $quoteItem->method('getCustomPrice')->willReturn(true); + /** @var MockObject|TaxDetailsItemInterface $itemTaxDetails */ + $itemTaxDetails = $this->getMockBuilder(TaxDetailsItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var MockObject|TaxDetailsItemInterface $baseItemTaxDetails */ + $baseItemTaxDetails = $this->getMockBuilder(TaxDetailsItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItem->expects($this->once())->method('setCustomPrice'); + + $this->commonTaxCollector->updateItemTaxInfo( + $quoteItem, + $itemTaxDetails, + $baseItemTaxDetails, + $this->store + ); + } + + /** + * Data for testGetShippingDataObject + * * @return array */ - public function getShippingDataObjectDataProvider() + public function getShippingDataObjectDataProvider(): array { $data = [ 'free_shipping' => [ diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php index 77e25d6f14574..2bfebc984bb81 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php @@ -133,7 +133,7 @@ private function getMockObject($className, array $objectState) $getterValueMap = []; $methods = ['__wakeup']; foreach ($objectState as $key => $value) { - $getterName = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); + $getterName = 'get' . str_replace('_', '', ucwords($key, '_')); $getterValueMap[$getterName] = $value; $methods[] = $getterName; } diff --git a/app/code/Magento/Tax/etc/di.xml b/app/code/Magento/Tax/etc/di.xml index 096f8359fadd3..3b46b0f9e258c 100644 --- a/app/code/Magento/Tax/etc/di.xml +++ b/app/code/Magento/Tax/etc/di.xml @@ -143,6 +143,7 @@ <arguments> <argument name="fieldMapping" xsi:type="array"> <item name="id" xsi:type="string">tax_calculation_rule_id</item> + <item name="code" xsi:type="string">main_table.code</item> <item name="tax_rate_ids" xsi:type="string">tax_calculation_rate_id</item> <item name="customer_tax_class_ids" xsi:type="string">cd.customer_tax_class_id</item> <item name="product_tax_class_ids" xsi:type="string">cd.product_tax_class_id</item> @@ -154,6 +155,7 @@ <arguments> <argument name="fieldMapping" xsi:type="array"> <item name="id" xsi:type="string">tax_calculation_rule_id</item> + <item name="code" xsi:type="string">main_table.code</item> <item name="tax_rate_ids" xsi:type="string">tax_calculation_rate_id</item> <item name="customer_tax_class_ids" xsi:type="string">cd.customer_tax_class_id</item> <item name="product_tax_class_ids" xsi:type="string">cd.product_tax_class_id</item> diff --git a/app/code/Magento/Tax/etc/extension_attributes.xml b/app/code/Magento/Tax/etc/extension_attributes.xml index 90a5e6d2ecee3..41af1df836d6f 100644 --- a/app/code/Magento/Tax/etc/extension_attributes.xml +++ b/app/code/Magento/Tax/etc/extension_attributes.xml @@ -20,4 +20,7 @@ <extension_attributes for="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface"> <attribute code="tax_adjustments" type="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface" /> </extension_attributes> + <extension_attributes for="Magento\Tax\Api\Data\QuoteDetailsItemInterface"> + <attribute code="price_for_tax_calculation" type="float" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Tax/etc/sales.xml b/app/code/Magento/Tax/etc/sales.xml index 64d29ece898de..15afd499bce3f 100644 --- a/app/code/Magento/Tax/etc/sales.xml +++ b/app/code/Magento/Tax/etc/sales.xml @@ -9,7 +9,7 @@ <section name="quote"> <group name="totals"> <item name="tax_subtotal" instance="Magento\Tax\Model\Sales\Total\Quote\Subtotal" sort_order="200"/> - <item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="300"/> + <item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="375"/> <item name="tax" instance="Magento\Tax\Model\Sales\Total\Quote\Tax" sort_order="450"> <renderer name="adminhtml" instance="Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax"/> <renderer name="frontend" instance="Magento\Tax\Block\Checkout\Tax"/> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/class/page/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/class/page/edit.phtml deleted file mode 100644 index 18e86549a1ff9..0000000000000 --- a/app/code/Magento/Tax/view/adminhtml/templates/class/page/edit.phtml +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -?> -<div data-mage-init='{"floatingHeader": {}}' class="page-actions"> - <?= $block->getBackButtonHtml() ?> - <?= $block->getResetButtonHtml() ?> - <?= $block->getDeleteButtonHtml() ?> - <?= $block->getSaveButtonHtml() ?> -</div> -<?= $block->getRenameFormHtml() ?> -<script type="text/x-magento-init"> - { - "#<?= /* @escapeNotVerified */ $block->getRenameFormId() ?>": { - "Magento_Tax/js/page/validate": {} - } - } -</script> diff --git a/app/code/Magento/Tax/view/adminhtml/web/js/page/validate.js b/app/code/Magento/Tax/view/adminhtml/web/js/page/validate.js deleted file mode 100644 index a49f199ba56b6..0000000000000 --- a/app/code/Magento/Tax/view/adminhtml/web/js/page/validate.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'mage/mage' -], function (jQuery) { - 'use strict'; - - return function (data, element) { - jQuery(element).mage('form').mage('validation'); - }; -}); diff --git a/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js b/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js index b21be98531ba9..2b1f387f5c8c4 100644 --- a/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js +++ b/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js @@ -12,13 +12,16 @@ define([ 'Magento_Checkout/js/view/summary/abstract-total', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/totals', - 'mage/translate' -], function (ko, Component, quote, totals, $t) { + 'mage/translate', + 'underscore' +], function (ko, Component, quote, totals, $t, _) { 'use strict'; var isTaxDisplayedInGrandTotal = window.checkoutConfig.includeTaxInGrandTotal, isFullTaxSummaryDisplayed = window.checkoutConfig.isFullTaxSummaryDisplayed, - isZeroTaxDisplayed = window.checkoutConfig.isZeroTaxDisplayed; + isZeroTaxDisplayed = window.checkoutConfig.isZeroTaxDisplayed, + taxAmount = 0, + rates = 0; return Component.extend({ defaults: { @@ -98,6 +101,33 @@ define([ return this.getFormattedPrice(amount); }, + /** + * @param {*} parent + * @param {*} percentage + * @return {*|String} + */ + getTaxAmount: function (parent, percentage) { + var totalPercentage = 0; + + taxAmount = parent.amount; + rates = parent.rates; + _.each(rates, function (rate) { + totalPercentage += parseFloat(rate.percent); + }); + + return this.getFormattedPrice(this.getPercentAmount(taxAmount, totalPercentage, percentage)); + }, + + /** + * @param {*} amount + * @param {*} totalPercentage + * @param {*} percentage + * @return {*|String} + */ + getPercentAmount: function (amount, totalPercentage, percentage) { + return parseFloat(amount * percentage / totalPercentage); + }, + /** * @return {Array} */ diff --git a/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html b/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html index 9c45e73db6fa4..45c468096abe1 100644 --- a/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html +++ b/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html @@ -32,18 +32,16 @@ <!-- ko if: !percent --> <th class="mark" scope="row" colspan="1" data-bind="text: title"></th> <!-- /ko --> - <!-- ko if: $index() == 0 --> - <td class="amount" rowspan="1"> - <!-- ko if: $parents[1].isCalculated() --> - <span class="price" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - <!-- ko ifnot: $parents[1].isCalculated() --> - <span class="not-calculated" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - </td> - <!-- /ko --> + <td class="amount" rowspan="1"> + <!-- ko if: $parents[1].isCalculated() --> + <span class="price" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + <!-- ko ifnot: $parents[1].isCalculated() --> + <span class="not-calculated" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + </td> </tr> <!-- /ko --> <!-- /ko --> diff --git a/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html b/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html index 0f2e3251bcfdb..5f1ac86e38ffd 100644 --- a/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html +++ b/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html @@ -43,18 +43,16 @@ <!-- ko if: !percent --> <th class="mark" scope="row" data-bind="text: title"></th> <!-- /ko --> - <!-- ko if: $index() == 0 --> - <td class="amount"> - <!-- ko if: $parents[1].isCalculated() --> - <span class="price" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - <!-- ko ifnot: $parents[1].isCalculated() --> - <span class="not-calculated" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - </td> - <!-- /ko --> + <td class="amount"> + <!-- ko if: $parents[1].isCalculated() --> + <span class="price" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + <!-- ko ifnot: $parents[1].isCalculated() --> + <span class="not-calculated" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + </td> </tr> <!-- /ko --> <!-- /ko --> diff --git a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php index e87e70e21b9de..89636ad3de50e 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php @@ -4,19 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Controller\Adminhtml\System\Design\Theme; +use Magento\Framework\App\Action\HttpGetActionInterface; + /** * Class UploadJs * @deprecated */ -class UploadJs extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme +class UploadJs extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme implements HttpGetActionInterface { /** * Upload js file * * @return void - * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { @@ -50,6 +52,7 @@ public function execute() \Magento\Framework\View\Design\Theme\Customization\File\Js::TYPE ); $result = ['error' => false, 'files' => $customization->generateFileInfo($customJsFiles)]; + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $result = ['error' => true, 'message' => $e->getMessage()]; } catch (\Exception $e) { diff --git a/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php b/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php new file mode 100644 index 0000000000000..a81f29280af96 --- /dev/null +++ b/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Theme\Controller\Result; + +/** + * Plugin for putting messages to cookies + */ +class JsFooterPlugin +{ + /** + * Put all javascript to footer before sending the response + * + * @param \Magento\Framework\App\Response\Http $subject + * @return void + */ + public function beforeSendResponse(\Magento\Framework\App\Response\Http $subject) + { + $content = $subject->getContent(); + $script = []; + if (strpos($content, '</body') !== false) { + $pattern = '#<script[^>]*+(?<!text/x-magento-template.)>.*?</script>#is'; + $content = preg_replace_callback( + $pattern, + function ($matchPart) use (&$script) { + $script[] = $matchPart[0]; + return ''; + }, + $content + ); + $subject->setContent( + str_replace('</body', implode("\n", $script) . "\n</body", $content) + ); + } + } +} diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index b37628e54aa30..8d1884671c3fb 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Model\Design\Backend; use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; @@ -22,6 +23,8 @@ use Magento\Theme\Model\Design\Config\FileUploader\FileProcessor; /** + * File Backend + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class File extends BackendFile @@ -88,36 +91,26 @@ public function beforeSave() { $values = $this->getValue(); $value = reset($values) ?: []; - if (!isset($value['file'])) { + + // Need to check name when it is uploaded in the media gallary + $file = $value['file'] ?? $value['name'] ?? null; + if (!isset($file)) { throw new LocalizedException( __('%1 does not contain field \'file\'', $this->getData('field_config/field')) ); } if (isset($value['exists'])) { - $this->setValue($value['file']); + $this->setValue($file); return $this; } - $filename = basename($value['file']); - $result = $this->_mediaDirectory->copyFile( - $this->getTmpMediaPath($filename), - $this->_getUploadDir() . '/' . $filename - ); - if ($result) { - $this->_mediaDirectory->delete($this->getTmpMediaPath($filename)); - if ($this->_addWhetherScopeInfo()) { - $filename = $this->_prependScopeInfo($filename); - } - $this->setValue($filename); - } else { - $this->unsValue(); - } + $this->updateMediaDirectory(basename($file), $value['url']); return $this; } /** - * @return array + * @inheritDoc */ public function afterLoad() { @@ -166,6 +159,8 @@ protected function getUploadDirPath($uploadDir) } /** + * Get Value + * * @return array */ public function getValue() @@ -231,4 +226,49 @@ private function getMime() } return $this->mime; } + + /** + * Get Relative Media Path + * + * @param string $path + * @return string + */ + private function getRelativeMediaPath(string $path): string + { + return str_replace('/pub/media/', '', $path); + } + + /** + * Move file to the correct media directory + * + * @param string $filename + * @param string $url + * @throws LocalizedException + */ + private function updateMediaDirectory(string $filename, string $url) + { + $relativeMediaPath = $this->getRelativeMediaPath($url); + $tmpMediaPath = $this->getTmpMediaPath($filename); + $mediaPath = $this->_mediaDirectory->isFile($relativeMediaPath) ? $relativeMediaPath : $tmpMediaPath; + $destinationMediaPath = $this->_getUploadDir() . '/' . $filename; + + $result = $mediaPath === $destinationMediaPath; + if (!$result) { + $result = $this->_mediaDirectory->copyFile( + $mediaPath, + $destinationMediaPath + ); + } + if ($result) { + if ($mediaPath === $tmpMediaPath) { + $this->_mediaDirectory->delete($mediaPath); + } + if ($this->_addWhetherScopeInfo()) { + $filename = $this->_prependScopeInfo($filename); + } + $this->setValue($filename); + } else { + $this->unsValue(); + } + } } diff --git a/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php b/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php index ecf5bcbea6dfc..901dbf7d88f3d 100644 --- a/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php +++ b/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php @@ -18,6 +18,8 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Design file processor. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FileProcessor @@ -79,8 +81,8 @@ public function __construct( * Save file to temp media directory * * @param string $fileId + * * @return array - * @throws LocalizedException */ public function saveToTmp($fileId) { diff --git a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php index 98fa12ab987b6..13b8aa23073ce 100644 --- a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php +++ b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php @@ -27,6 +27,11 @@ class Builder implements \Magento\Framework\View\Model\PageLayout\Config\Builder */ protected $themeCollection; + /** + * @var array + */ + private $configFiles = []; + /** * @param \Magento\Framework\View\PageLayout\ConfigFactory $configFactory * @param \Magento\Framework\View\PageLayout\File\Collector\Aggregated $fileCollector @@ -44,7 +49,7 @@ public function __construct( } /** - * @return \Magento\Framework\View\PageLayout\Config + * @inheritdoc */ public function getPageLayoutsConfig() { @@ -52,15 +57,20 @@ public function getPageLayoutsConfig() } /** + * Retrieve configuration files. + * * @return array */ protected function getConfigFiles() { - $configFiles = []; - foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { - $configFiles = array_merge($configFiles, $this->fileCollector->getFilesContent($theme, 'layouts.xml')); + if (!$this->configFiles) { + $configFiles = []; + foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { + $configFiles[] = $this->fileCollector->getFilesContent($theme, 'layouts.xml'); + } + $this->configFiles = array_merge(...$configFiles); } - return $configFiles; + return $this->configFiles; } } diff --git a/app/code/Magento/Theme/Model/ResourceModel/Design.php b/app/code/Magento/Theme/Model/ResourceModel/Design.php index c36640b98ef40..6711f4a2117be 100644 --- a/app/code/Magento/Theme/Model/ResourceModel/Design.php +++ b/app/code/Magento/Theme/Model/ResourceModel/Design.php @@ -46,7 +46,7 @@ protected function _construct() * Perform actions before object save * * @param \Magento\Framework\Model\AbstractModel $object - * @return $this + * @return void * @throws \Magento\Framework\Exception\LocalizedException */ public function _beforeSave(\Magento\Framework\Model\AbstractModel $object) @@ -152,7 +152,6 @@ protected function _checkIntersection($storeId, $dateFrom, $dateTo, $currentId) $dateConditions = []; } - $condition = ''; if (!empty($dateConditions)) { $condition = '(' . implode(') OR (', $dateConditions) . ')'; $select->where($condition); diff --git a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php index 2c3350e695a85..0519460c02423 100644 --- a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php +++ b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php @@ -4,17 +4,12 @@ * See COPYING.txt for license details. */ -/** - * Theme wysiwyg storage model - */ namespace Magento\Theme\Model\Wysiwyg; use Magento\Framework\App\Filesystem\DirectoryList; /** - * Class Storage - * - * @package Magento\Theme\Model\Wysiwyg + * Theme wysiwyg storage model */ class Storage { @@ -110,7 +105,7 @@ public function __construct( * Upload file * * @param string $targetPath - * @return bool + * @return array * @throws \Magento\Framework\Exception\LocalizedException */ public function uploadFile($targetPath) @@ -271,7 +266,7 @@ public function getFilesCollection() if (self::TYPE_IMAGE == $storageType) { $requestParams['file'] = $fileName; $file['thumbnailParams'] = $requestParams; - + //phpcs:ignore Generic.PHP.NoSilencedErrors $size = @getimagesize($path); if (is_array($size)) { $file['width'] = $size[0]; diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml new file mode 100644 index 0000000000000..6b98686574321 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.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="NavigateToFaviconMediaFolderActionGroup"> + <arguments> + <argument name="StoreFolder" type="string"/> + </arguments> + <conditionalClick selector="{{MediaGallerySection.StorageRootArrow}}" dependentSelector="{{MediaGallerySection.checkIfArrowExpand}}" stepKey="clickArrowIfClosed" visible="true"/> + <waitForElement selector="{{AdminDesignConfigSection.faviconArrow}}" stepKey="waitForFaviconFolder"/> + <conditionalClick selector="{{AdminDesignConfigSection.faviconArrow}}" dependentSelector="{{AdminDesignConfigSection.checkIfFaviconArrowExpand}}" stepKey="clickFaviconArrowIfClosed" visible="true"/> + <waitForElement selector="{{AdminDesignConfigSection.storesArrow}}" stepKey="waitForStoresFolder"/> + <conditionalClick selector="{{AdminDesignConfigSection.storesArrow}}" dependentSelector="{{AdminDesignConfigSection.checkIfStoresArrowExpand}}" stepKey="clickStoresArrowIfClosed" visible="true"/> + <waitForElement selector="{{StoreFolder}}" stepKey="waitForStoreFolder"/> + <click selector="{{StoreFolder}}" stepKey="clickOnCreatedFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontClickOnHeaderLogoActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontClickOnHeaderLogoActionGroup.xml new file mode 100644 index 0000000000000..cd4117a4cfa6e --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontClickOnHeaderLogoActionGroup.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="StorefrontClickOnHeaderLogoActionGroup"> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageLoaded"/> + <click selector="{{StorefrontHeaderSection.logoLink}}" stepKey="clickOnLogo"/> + <waitForPageLoad stepKey="waitForHomePageLoadedAfterClickOnLogo"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Theme/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..e826651062562 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Data/AdminMenuData.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="AdminMenuContentDesignThemes"> + <data key="pageTitle">Themes</data> + <data key="title">Themes</data> + <data key="dataUiId">magento-theme-system-design-theme</data> + </entity> +</entities> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml index 0aa2f7f35218a..c2652f33f7606 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml @@ -14,5 +14,22 @@ <element name="watermarkSection" type="text" selector="[data-index='watermark'] .admin__fieldset-wrapper-content"/> <element name="imageUploadInputByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//input" parameterized="true"/> <element name="imageUploadPreviewByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader-preview')]//img" parameterized="true"/> + <element name="addSelectedFromMediaGallery" type="input" selector="//button[contains(@title,'Add Selected')]"/> + <element name="htmlHeaderSection" type="text" selector="[data-index='head']"/> + <element name="selectFromGalleryByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//label[contains(text(), 'Select from Gallery')]" parameterized="true"/> + <element name="imageUploadFromMediaGallery" type="input" selector="//input[contains(@class,'fileupload')]" /> + <element name="saveConfiguration" type="input" selector="//button[contains(@title, 'Save Configuration')]" /> + <element name="successNotification" type="text" selector="//div[contains(@data-ui-id, 'messages-message-success')]" /> + <element name="useDefaultByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//span[contains(text(), 'Use Default Value')]" parameterized="true" /> + <element name="logoSectionHeader" type="text" selector="[data-index='email']"/> + <element name="logoSection" type="text" selector="[data-index='email'] .admin__fieldset-wrapper-content"/> + <element name="logoUpload" type ="input" selector="[name='email_logo']" /> + <element name="logoWrapperOpen" type ="text" selector="[data-index='email'] [data-state-collapsible ='closed']"/> + <element name="logoPreview" type ="text" selector="[alt ='magento-logo.png']"/> + <element name="faviconArrow" type="button" selector="#ZmF2aWNvbg-- > .jstree-icon" /> + <element name="checkIfFaviconArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbg--' and contains(@class,'jstree-closed')]" /> + <element name="storesArrow" type="button" selector="#ZmF2aWNvbi9zdG9yZXM- > .jstree-icon" /> + <element name="checkIfStoresArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbi9zdG9yZXM-' and contains(@class,'jstree-closed')]" /> + <element name="storeLink" type="button" selector="#ZmF2aWNvbi9zdG9yZXMvMQ-- > a"/> </section> </sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml index a4088c7a4a0b7..e2f0a01fc733b 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,5 +9,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> <element name="welcomeMessage" type="text" selector=".greet.welcome"/> + <element name="logoLink" type="button" selector=".header .logo"/> </section> </sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml new file mode 100644 index 0000000000000..8e7bfd71b07d3 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminContentThemesNavigateMenuTest"> + <annotations> + <features value="Theme"/> + <stories value="Menu Navigation"/> + <title value="Admin content themes navigate menu test"/> + <description value="Admin should be able to navigate to Content > Themes"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14112"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentThemesPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentDesignThemes.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentDesignThemes.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml new file mode 100644 index 0000000000000..f46328ac151b1 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.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="AdminDesignConfigMediaGalleryImageUploadTest"> + <annotations> + <features value="Content"/> + <stories value="Content"/> + <title value="MC-5784: Image fields using imageUploader UIComponent cannot use gallery image"/> + <description value="Admin should be able to use Image Uploader to add Gallery Images"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13832"/> + <group value="Content"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Edit Store View--> + <comment userInput="Edit Store View" stepKey="editStoreViewComment"/> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage" /> + <waitForPageLoad stepKey="waitForPageload1"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView"/> + <waitForPageLoad stepKey="waitForPageload2"/> + <scrollTo selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="scrollToHtmlHeadSection"/> + <click selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="openHtmlHeadSection"/> + <!--Upload Image--> + <comment userInput="Upload Image" stepKey="uploadImageComment"/> + <click selector="{{AdminDesignConfigSection.selectFromGalleryByFieldsetName('Head')}}" stepKey="openMediaGallery"/> + <actionGroup ref="VerifyMediaGalleryStorageActions" stepKey="verifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="attachImage" stepKey="selectImageFromMediaStorage"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="saveImage" stepKey="insertImage"/> + <click selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="saveConfiguration"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.successNotification}}" stepKey="waitForSuccessNotification"/> + <waitForPageLoad stepKey="waitForPageloadSuccess"/> + <!--Edit Store View--> + <comment userInput="Edit Store View" stepKey="editStoreViewComment2"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView2"/> + <waitForPageLoad stepKey="waitForPageload3"/> + <scrollTo selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="scrollToHtmlHeadSection2"/> + <click selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="openHtmlHeadSection2"/> + <!--Save Default Configuration--> + <comment userInput="Save Default Configuration" stepKey="saveDefaultConfigurationComment"/> + <click selector="{{AdminDesignConfigSection.useDefaultByFieldsetName('Head')}}" stepKey="clickUseDefault"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="waitForWrapperToClose2"/> + <click selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="saveConfiguration2"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.successNotification}}" stepKey="waitForSuccessNotification2"/> + <waitForPageLoad stepKey="waitForPageloadSuccess2"/> + <!--Delete Image: will be in both root and favicon--> + <comment userInput="Delete Image" stepKey="deleteImageComment"/> + <actionGroup ref="navigateToMediaGallery" stepKey="navigateToMediaGallery"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder2"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="DeleteImageFromStorageActionGroup" stepKey="deleteImageFromStorage"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="NavigateToFaviconMediaFolderActionGroup" stepKey="navigateToFolder3"> + <argument name="StoreFolder" value="{{AdminDesignConfigSection.storeLink}}"/> + </actionGroup> + <actionGroup ref="DeleteImageFromStorageActionGroup" stepKey="deleteImageFromStorage2"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php b/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php index e5d69cbc820a1..8429be84cae44 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php @@ -83,7 +83,7 @@ public function testGetPageLayoutsConfig() ->disableOriginalConstructor() ->getMock(); - $this->themeCollection->expects($this->any()) + $this->themeCollection->expects($this->once()) ->method('loadRegisteredThemes') ->willReturn([$theme1, $theme2]); diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 148267feeaad0..62f51e74b6007 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -273,7 +273,7 @@ <type name="Magento\Config\App\Config\Source\DumpConfigSourceAggregated"> <plugin name="designConfigTheme" type="Magento\Theme\Model\Design\Config\Plugin\Dump" sortOrder="50"/> </type> - <type name="\Magento\Theme\Model\Design\Config\Plugin\Dump"> + <type name="Magento\Theme\Model\Design\Config\Plugin\Dump"> <arguments> <argument name="themeList" xsi:type="object">Magento\Theme\Model\ResourceModel\Theme\Collection</argument> </arguments> diff --git a/app/code/Magento/Theme/etc/frontend/di.xml b/app/code/Magento/Theme/etc/frontend/di.xml index 7db2783cd8dfa..3837c6f717b54 100644 --- a/app/code/Magento/Theme/etc/frontend/di.xml +++ b/app/code/Magento/Theme/etc/frontend/di.xml @@ -26,4 +26,7 @@ <type name="Magento\Framework\Controller\ResultInterface"> <plugin name="result-messages" type="Magento\Theme\Controller\Result\MessagePlugin"/> </type> + <type name="Magento\Framework\App\Response\Http"> + <plugin name="result-js-footer" type="Magento\Theme\Controller\Result\JsFooterPlugin"/> + </type> </config> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml index 79b891b7e55e6..9c34dfea3218b 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml @@ -12,10 +12,14 @@ ?> <?php $storeName = $block->getThemeName() ? $block->getThemeName() : $block->getLogoAlt();?> <span data-action="toggle-nav" class="action nav-toggle"><span><?= /* @escapeNotVerified */ __('Toggle Nav') ?></span></span> -<a class="logo" href="<?= $block->getUrl('') ?>" title="<?= /* @escapeNotVerified */ $storeName ?>"> +<a + class="logo" + href="<?= $block->getUrl('') ?>" + title="<?= /* @escapeNotVerified */ $storeName ?>" + aria-label="store logo"> <img src="<?= /* @escapeNotVerified */ $block->getLogoSrc() ?>" - title="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" - alt="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" + title="<?= $block->escapeHtmlAttr($block->getLogoAlt()) ?>" + alt="<?= $block->escapeHtmlAttr($block->getLogoAlt()) ?>" <?= $block->getLogoWidth() ? 'width="' . $block->getLogoWidth() . '"' : '' ?> <?= $block->getLogoHeight() ? 'height="' . $block->getLogoHeight() . '"' : '' ?> /> diff --git a/app/code/Magento/ThemeGraphQl/etc/graphql/di.xml b/app/code/Magento/ThemeGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..9f55e522bf5a1 --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/etc/graphql/di.xml @@ -0,0 +1,30 @@ +<?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\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="head_shortcut_icon" xsi:type="string">design/head/shortcut_icon</item> + <item name="default_title" xsi:type="string">design/head/default_title</item> + <item name="title_prefix" xsi:type="string">design/head/title_prefix</item> + <item name="title_suffix" xsi:type="string">design/head/title_suffix</item> + <item name="default_description" xsi:type="string">design/head/default_description</item> + <item name="default_keywords" xsi:type="string">design/head/default_keywords</item> + <item name="head_includes" xsi:type="string">design/head/includes</item> + <item name="demonotice" xsi:type="string">design/head/demonotice</item> + <item name="header_logo_src" xsi:type="string">design/header/logo_src</item> + <item name="logo_width" xsi:type="string">design/header/logo_width</item> + <item name="logo_height" xsi:type="string">design/header/logo_height</item> + <item name="logo_alt" xsi:type="string">design/header/logo_alt</item> + <item name="welcome" xsi:type="string">design/header/welcome</item> + <item name="absolute_footer" xsi:type="string">design/footer/absolute_footer</item> + <item name="copyright" xsi:type="string">design/footer/copyright</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/Formatter.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/Formatter.js index f74282afd32a6..b7266779d3a35 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/Formatter.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/Formatter.js @@ -1970,7 +1970,7 @@ node.appendChild(dom.doc.createTextNode(invisibleChar)); node = node.firstChild; - // Insert caret container after the formated node + // Insert caret container after the formatted node dom.insertAfter(caretContainer, formatNode); // Move selection to text node diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js index 2d9d859caa6fa..60dd358414be1 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js @@ -15796,7 +15796,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { node.appendChild(dom.doc.createTextNode(invisibleChar)); node = node.firstChild; - // Insert caret container after the formated node + // Insert caret container after the formatted node dom.insertAfter(caretContainer, formatNode); // Move selection to text node diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js index aaa207da3e4a9..ed0b7cb0e50a2 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js @@ -16646,7 +16646,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { node.appendChild(dom.doc.createTextNode(invisibleChar)); node = node.firstChild; - // Insert caret container after the formated node + // Insert caret container after the formatted node dom.insertAfter(caretContainer, formatNode); // Move selection to text node diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js index 8448152ed5d2f..1c53062dd9690 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js @@ -16620,7 +16620,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { node.appendChild(dom.doc.createTextNode(invisibleChar)); node = node.firstChild; - // Insert caret container after the formated node + // Insert caret container after the formatted node dom.insertAfter(caretContainer, formatNode); // Move selection to text node diff --git a/app/code/Magento/Translation/Block/Js.php b/app/code/Magento/Translation/Block/Js.php index 86bb416524d8f..db26feb8067ff 100644 --- a/app/code/Magento/Translation/Block/Js.php +++ b/app/code/Magento/Translation/Block/Js.php @@ -8,9 +8,10 @@ use Magento\Framework\View\Element\Template; use Magento\Translation\Model\Js\Config; -use Magento\Framework\Escaper; /** + * JS translation block + * * @api * @since 100.0.2 */ @@ -54,7 +55,7 @@ public function dictionaryEnabled() } /** - * gets current js-translation.json timestamp + * Gets current js-translation.json timestamp * * @return string */ @@ -64,6 +65,8 @@ public function getTranslationFileTimestamp() } /** + * Get translation file path + * * @return string */ public function getTranslationFilePath() diff --git a/app/code/Magento/Translation/Model/Json/PreProcessor.php b/app/code/Magento/Translation/Model/Json/PreProcessor.php index 5d46c3c8b0618..c178a324cb40b 100644 --- a/app/code/Magento/Translation/Model/Json/PreProcessor.php +++ b/app/code/Magento/Translation/Model/Json/PreProcessor.php @@ -6,6 +6,7 @@ namespace Magento\Translation\Model\Json; +use Magento\Framework\App\Area; use Magento\Framework\App\AreaList; use Magento\Framework\App\ObjectManager; use Magento\Framework\TranslateInterface; @@ -13,6 +14,7 @@ use Magento\Framework\View\Asset\PreProcessor\Chain; use Magento\Framework\View\Asset\PreProcessorInterface; use Magento\Framework\View\DesignInterface; +use Magento\Backend\App\Area\FrontNameResolver; use Magento\Translation\Model\Js\Config; use Magento\Translation\Model\Js\DataProviderInterface; @@ -83,7 +85,7 @@ public function process(Chain $chain) $context = $chain->getAsset()->getContext(); $themePath = '*/*'; - $areaCode = \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE; + $areaCode = FrontNameResolver::AREA_CODE; if ($context instanceof FallbackContext) { $themePath = $context->getThemePath(); @@ -92,8 +94,10 @@ public function process(Chain $chain) $this->viewDesign->setDesignTheme($themePath, $areaCode); } - $area = $this->areaList->getArea($areaCode); - $area->load(\Magento\Framework\App\Area::PART_TRANSLATE); + if ($areaCode !== FrontNameResolver::AREA_CODE) { + $area = $this->areaList->getArea($areaCode); + $area->load(Area::PART_TRANSLATE); + } $this->translate->setLocale($context->getLocale())->loadData($areaCode, true); diff --git a/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php b/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php index d9340e03dc996..cbeeefed6be6e 100644 --- a/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php +++ b/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php @@ -8,39 +8,43 @@ use Magento\Translation\Model\Js\Config; use Magento\Translation\Model\Js\DataProvider; use Magento\Translation\Model\Json\PreProcessor; +use Magento\Backend\App\Area\FrontNameResolver; class PreProcessorTest extends \PHPUnit\Framework\TestCase { /** * @var PreProcessor */ - protected $model; + private $model; /** * @var Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $configMock; + private $configMock; /** * @var DataProvider|\PHPUnit_Framework_MockObject_MockObject */ - protected $dataProviderMock; + private $dataProviderMock; /** * @var \Magento\Framework\App\AreaList|\PHPUnit_Framework_MockObject_MockObject */ - protected $areaListMock; + private $areaListMock; /** * @var \Magento\Framework\TranslateInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $translateMock; + private $translateMock; /** * @var \Magento\Framework\View\DesignInterface|\PHPUnit_Framework_MockObject_MockObject */ private $designMock; + /** + * @inheritdoc + */ protected function setUp() { $this->configMock = $this->createMock(\Magento\Translation\Model\Js\Config::class); @@ -57,7 +61,14 @@ protected function setUp() ); } - public function testGetData() + /** + * Test 'process' method. + * + * @param array $data + * @param array $expects + * @dataProvider processDataProvider + */ + public function testProcess(array $data, array $expects) { $chain = $this->createMock(\Magento\Framework\View\Asset\PreProcessor\Chain::class); $asset = $this->createMock(\Magento\Framework\View\Asset\File::class); @@ -66,8 +77,10 @@ public function testGetData() $targetPath = 'path/js-translation.json'; $themePath = '*/*'; $dictionary = ['hello' => 'bonjour']; - $areaCode = 'adminhtml'; + $areaCode = $data['area_code']; + $area = $this->createMock(\Magento\Framework\App\Area::class); + $area->expects($expects['area_load'])->method('load')->willReturnSelf(); $chain->expects($this->once()) ->method('getTargetAssetPath') @@ -93,7 +106,7 @@ public function testGetData() $this->designMock->expects($this->once())->method('setDesignTheme')->with($themePath, $areaCode); - $this->areaListMock->expects($this->once()) + $this->areaListMock->expects($expects['areaList_getArea']) ->method('getArea') ->with($areaCode) ->willReturn($area); @@ -114,4 +127,33 @@ public function testGetData() $this->model->process($chain); } + + /** + * Data provider for 'process' method test. + * + * @return array + */ + public function processDataProvider() + { + return [ + [ + [ + 'area_code' => FrontNameResolver::AREA_CODE + ], + [ + 'areaList_getArea' => $this->never(), + 'area_load' => $this->never(), + ] + ], + [ + [ + 'area_code' => 'frontend' + ], + [ + 'areaList_getArea' => $this->once(), + 'area_load' => $this->once(), + ] + ], + ]; + } } diff --git a/app/code/Magento/Translation/view/base/templates/translate.phtml b/app/code/Magento/Translation/view/base/templates/translate.phtml index c8366037e2294..ec88b1d092026 100644 --- a/app/code/Magento/Translation/view/base/templates/translate.phtml +++ b/app/code/Magento/Translation/view/base/templates/translate.phtml @@ -9,19 +9,50 @@ /** @var \Magento\Translation\Block\Js $block */ ?> <?php if ($block->dictionaryEnabled()): ?> + <script> + require.config({ + deps: [ + 'jquery', + 'mage/translate', + 'jquery/jquery-storageapi' + ], + callback: function ($) { + 'use strict'; + + var dependencies = [], + versionObj; + + $.initNamespaceStorage('mage-translation-storage'); + $.initNamespaceStorage('mage-translation-file-version'); + versionObj = $.localStorage.get('mage-translation-file-version'); + + <?php $version = $block->getTranslationFileVersion(); ?> + + if (versionObj.version !== '<?= /* @escapeNotVerified */ $block->escapeJsQuote($version) ?>') { + dependencies.push( + 'text!<?= /* @noEscape */ Magento\Translation\Model\Js\Config::DICTIONARY_FILE_NAME ?>' + ); -<?php - $version = $block->getTranslationFileVersion(); - $fileName = Magento\Translation\Model\Js\Config::DICTIONARY_FILE_NAME; -?> - <script type="text/x-magento-init"> - { - "*": { - "mage/translate-init": { - "dictionaryFile": "text!<?= $block->escapeJs($fileName); ?>", - "version": "<?= $block->escapeJs($version) ?>" } + + require.config({ + deps: dependencies, + callback: function (string) { + if (typeof string === 'string') { + $.mage.translate.add(JSON.parse(string)); + $.localStorage.set('mage-translation-storage', string); + $.localStorage.set( + 'mage-translation-file-version', + { + version: '<?= /* @escapeNotVerified */ $block->escapeJsQuote($version) ?>' + } + ); + } else { + $.mage.translate.add($.localStorage.get('mage-translation-storage')); + } + } + }); } - } + }); </script> <?php endif; ?> diff --git a/app/code/Magento/Ui/Component/Form.php b/app/code/Magento/Ui/Component/Form.php index 4033abba820e0..dc6e7b5ca06ab 100644 --- a/app/code/Magento/Ui/Component/Form.php +++ b/app/code/Magento/Ui/Component/Form.php @@ -10,6 +10,7 @@ use Magento\Framework\View\Element\UiComponentInterface; /** + * Ui component Form * @api * @since 100.0.2 */ @@ -53,14 +54,15 @@ public function getComponentName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDataSourceData() { $dataSource = []; $id = $this->getContext()->getRequestParam($this->getContext()->getDataProvider()->getRequestFieldName(), null); - $filter = $this->filterBuilder->setField($this->getContext()->getDataProvider()->getPrimaryFieldName()) + $idFieldName = $this->getContext()->getDataProvider()->getPrimaryFieldName(); + $filter = $this->filterBuilder->setField($idFieldName) ->setValue($id) ->create(); $this->getContext()->getDataProvider() @@ -74,7 +76,7 @@ public function getDataSourceData() ]; } elseif (isset($data['items'])) { foreach ($data['items'] as $item) { - if ($item[$item['id_field_name']] == $id) { + if ($item[$idFieldName] == $id) { $dataSource = ['data' => ['general' => $item]]; } } diff --git a/app/code/Magento/Ui/Component/MassAction.php b/app/code/Magento/Ui/Component/MassAction.php index ea39e19a65f52..4cca8d4c012bb 100644 --- a/app/code/Magento/Ui/Component/MassAction.php +++ b/app/code/Magento/Ui/Component/MassAction.php @@ -6,6 +6,8 @@ namespace Magento\Ui\Component; /** + * Mass action UI component. + * * @api * @since 100.0.2 */ @@ -21,7 +23,12 @@ public function prepare() $config = $this->getConfiguration(); foreach ($this->getChildComponents() as $actionComponent) { - $config['actions'][] = $actionComponent->getConfiguration(); + $componentConfig = $actionComponent->getConfiguration(); + $disabledAction = $componentConfig['actionDisable'] ?? false; + if ($disabledAction) { + continue; + } + $config['actions'][] = $componentConfig; } $origConfig = $this->getConfiguration(); diff --git a/app/code/Magento/Ui/Component/MassAction/Filter.php b/app/code/Magento/Ui/Component/MassAction/Filter.php index a8ed5d901d860..c512c82d694bc 100644 --- a/app/code/Magento/Ui/Component/MassAction/Filter.php +++ b/app/code/Magento/Ui/Component/MassAction/Filter.php @@ -7,14 +7,16 @@ namespace Magento\Ui\Component\MassAction; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\App\RequestInterface; -use Magento\Framework\View\Element\UiComponentInterface; use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponentInterface; /** + * Filter component. + * * @api * @since 100.0.2 */ @@ -100,9 +102,13 @@ public function getCollection(AbstractDb $collection) } } + $filterIds = $this->getFilterIds(); + if (\is_array($selected)) { + $filterIds = array_unique(array_merge($filterIds, $selected)); + } $collection->addFieldToFilter( $collection->getIdFieldName(), - ['in' => $this->getFilterIds()] + ['in' => $filterIds] ); return $collection; diff --git a/app/code/Magento/Ui/Component/Wysiwyg/Config.php b/app/code/Magento/Ui/Component/Wysiwyg/Config.php index 48014a0160c41..d88a255927876 100644 --- a/app/code/Magento/Ui/Component/Wysiwyg/Config.php +++ b/app/code/Magento/Ui/Component/Wysiwyg/Config.php @@ -13,7 +13,7 @@ class Config implements ConfigInterface /** * Return WYSIWYG configuration * - * @return \Magento\Framework\DataObject + * @return array */ public function getConfig() { diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php index b983e56b8aee2..b06c655939b1c 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php @@ -86,6 +86,18 @@ public function execute() $contentType = $this->contentTypeResolver->resolve($component->getContext()); $this->getResponse()->setHeader('Content-Type', $contentType, true); + } else { + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setStatusHeader( + \Zend\Http\Response::STATUS_CODE_403, + \Zend\Http\AbstractMessage::VERSION_11, + 'Forbidden' + ); + return $resultJson->setData([ + 'error' => $this->escaper->escapeHtml('Forbidden'), + 'errorcode' => 403 + ]); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->logger->critical($e); diff --git a/app/code/Magento/Ui/Model/UiComponentGenerator.php b/app/code/Magento/Ui/Model/UiComponentGenerator.php index f699cff7aa528..ce51c4241e86d 100644 --- a/app/code/Magento/Ui/Model/UiComponentGenerator.php +++ b/app/code/Magento/Ui/Model/UiComponentGenerator.php @@ -32,7 +32,6 @@ class UiComponentGenerator * UiComponentGenerator constructor. * @param ContextFactory $contextFactory * @param UiComponentFactory $uiComponentFactory - * @param array $data */ public function __construct( ContextFactory $contextFactory, @@ -48,6 +47,7 @@ public function __construct( * @param string $name * @param \Magento\Framework\View\LayoutInterface $layout * @return UiComponentInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function generateUiComponent($name, \Magento\Framework\View\LayoutInterface $layout) { diff --git a/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php b/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php index e249a64861d43..d16b1eaad7f37 100644 --- a/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php +++ b/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php @@ -5,6 +5,8 @@ */ namespace Magento\Ui\TemplateEngine\Xhtml; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\JsonHexTag; use Magento\Framework\View\Layout\Generator\Structure; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Framework\View\TemplateEngine\Xhtml\Template; @@ -42,25 +44,33 @@ class Result implements ResultInterface */ protected $logger; + /** + * @var JsonHexTag + */ + private $jsonSerializer; + /** * @param Template $template * @param CompilerInterface $compiler * @param UiComponentInterface $component * @param Structure $structure * @param LoggerInterface $logger + * @param JsonHexTag $jsonSerializer */ public function __construct( Template $template, CompilerInterface $compiler, UiComponentInterface $component, Structure $structure, - LoggerInterface $logger + LoggerInterface $logger, + JsonHexTag $jsonSerializer = null ) { $this->template = $template; $this->compiler = $compiler; $this->component = $component; $this->structure = $structure; $this->logger = $logger; + $this->jsonSerializer = $jsonSerializer ?? ObjectManager::getInstance()->get(JsonHexTag::class); } /** @@ -81,7 +91,7 @@ public function getDocumentElement() public function appendLayoutConfiguration() { $layoutConfiguration = $this->wrapContent( - json_encode($this->structure->generate($this->component), JSON_HEX_TAG) + $this->jsonSerializer->serialize($this->structure->generate($this->component)) ); $this->template->append($layoutConfiguration); } @@ -105,7 +115,7 @@ public function __toString() $this->compiler->compile($templateRootElement, $this->component, $this->component); $this->appendLayoutConfiguration(); $result = $this->compiler->postprocessing($this->template->__toString()); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->logger->critical($e->getMessage()); $result = $e->getMessage(); } diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml index 9148c22976c19..fbb543a6cab92 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml @@ -23,8 +23,23 @@ </arguments> <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown"/> <click selector="{{AdminDataGridPaginationSection.perPageOption('Custom')}}" stepKey="selectCustomPerPage"/> + <waitForElementVisible selector="{{AdminDataGridPaginationSection.perPageInput}}" time="30" stepKey="waitForInputVisible"/> <fillField selector="{{AdminDataGridPaginationSection.perPageInput}}" userInput="{{perPage}}" stepKey="fillCustomPerPage"/> <click selector="{{AdminDataGridPaginationSection.perPageApplyInput}}" stepKey="applyCustomPerPage"/> <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <seeInField selector="{{AdminDataGridPaginationSection.perPageDropDownValue}}" userInput="{{perPage}}" stepKey="seePerPageValueInDropDown"/> + </actionGroup> + + <actionGroup name="adminDataGridDeleteCustomPerPage"> + <arguments> + <argument name="perPage"/> + </arguments> + <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown1"/> + <click selector="{{AdminDataGridPaginationSection.perPageEditCustomValue(perPage)}}" stepKey="clickToEditCustomPerPage"/> + <waitForElementVisible selector="{{AdminDataGridPaginationSection.perPageDeleteCustomValue(perPage)}}" time="30" stepKey="waitForDeleteButtonVisible"/> + <click selector="{{AdminDataGridPaginationSection.perPageDeleteCustomValue(perPage)}}" stepKey="clickToDeleteCustomPerPage"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown"/> + <dontSeeElement selector="{{AdminDataGridPaginationSection.perPageDropDownItem(perPage)}}" stepKey="dontSeeDropDownItem"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml index 3e917a5944f95..4ee38e30f98e6 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml @@ -25,5 +25,7 @@ <!--Visible columns management--> <element name="columnsToggle" type="button" selector="div.admin__data-grid-action-columns button[data-bind='toggleCollapsible']" timeout="30"/> <element name="columnCheckbox" type="checkbox" selector="//div[contains(@class,'admin__data-grid-action-columns')]//div[contains(@class, 'admin__field-option')]//label[text() = '{{column}}']/preceding-sibling::input" parameterized="true"/> + <element name="perPage" type="select" selector="#product_attributes_listing.product_attributes_listing.listing_top.listing_paging_sizes"/> + <element name="attributeName" type="input" selector="//div[text()='{{arg}}']/../preceding-sibling::td//input" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 0f54f51549e7a..133836761174d 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -11,11 +11,15 @@ <section name="AdminDataGridPaginationSection"> <element name="perPageDropdown" type="select" selector=".admin__data-grid-pager-wrap .selectmenu"/> <element name="perPageOption" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//button[text()='{{var1}}']" parameterized="true"/> - <element name="perPageInput" type="input" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//div[@class='selectmenu-item-edit']//input"/> - <element name="perPageApplyInput" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//div[@class='selectmenu-item-edit']//button"/> + <element name="perPageInput" type="input" selector="//div[contains(@class, 'admin__data-grid-pager-wrap')]//div[contains(@class, 'selectmenu-items _active')]//li[contains(@class, '_edit')]//div[contains(@class, 'selectmenu-item-edit')]//input"/> + <element name="perPageApplyInput" type="button" selector="//div[contains(@class, 'admin__data-grid-pager-wrap')]//div[contains(@class, 'selectmenu-items _active')]//li[@class='_edit']//div[contains(@class, 'selectmenu-item-edit')]//button"/> + <element name="perPageDropDownItem" type="button" selector="//*[contains(@class, 'selectmenu-items _active')]//button[contains(@class, 'selectmenu-item-action') and text()='{{dropDownItem}}']" timeout="30" parameterized="true"/> + <element name="perPageEditCustomValue" type="button" selector="//div[contains(@class, 'selectmenu-items _active')]//div[contains(@class, 'selectmenu-item')]//button[text()='{{perPageCustomValue}}']/following-sibling::button[contains(@class, 'action-edit')]" parameterized="true"/> + <element name="perPageDeleteCustomValue" type="button" selector="//div[contains(@class, 'selectmenu-items _active')]//div[contains(@class, 'selectmenu-item')]//button[text()='{{perPageCustomValue}}']/parent::div/preceding-sibling::div/button[contains(@class, 'action-delete')]" parameterized="true"/> <element name="nextPage" type="button" selector="div.admin__data-grid-pager > button.action-next" timeout="30"/> <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="perPageDropDownValue" type="input" selector=".selectmenu-value input" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml index 75d445f1ee04e..3d4efa13ce3a0 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml @@ -12,5 +12,6 @@ <element name="successMessage" type="text" selector=".message-success"/> <element name="errorMessage" type="text" selector=".message.message-error.error"/> <element name="warningMessage" type="text" selector=".message-warning"/> + <element name="noticeMessage" type="text" selector=".message-notice"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php index 6f45c192d6c4c..2cb35c7b85ddc 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php @@ -13,13 +13,16 @@ class ActionDeleteTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return ActionDelete::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(ActionDelete::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php index 025f4a1582458..3f00fa6c7ff34 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php @@ -15,13 +15,16 @@ class CheckboxSetTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return CheckboxSet::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(CheckboxSet::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php index cb91fbb945bb5..f37ca38a8d9bc 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php @@ -15,13 +15,16 @@ class MultiSelectTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return MultiSelect::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->contextMock->expects($this->never())->method('getProcessor'); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php index 0e0fef60df60b..67150e3c8fd3c 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php @@ -15,13 +15,16 @@ class RadioSetTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return RadioSet::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(RadioSet::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php index c695262681063..d4677192cc084 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php @@ -15,13 +15,16 @@ class SelectTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return Select::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(Select::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php index b345989ba05ef..4bfd952a6c566 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php @@ -85,13 +85,16 @@ protected function getModel() } /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return Wysiwyg::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(Wysiwyg::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/FormTest.php b/app/code/Magento/Ui/Test/Unit/Component/FormTest.php index 6951583291df9..6df69c7d0e48d 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/FormTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/FormTest.php @@ -9,7 +9,6 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; -use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Ui\Component\Form; class FormTest extends \PHPUnit\Framework\TestCase @@ -215,4 +214,65 @@ public function testGetDataSourceDataWithoutId() $this->assertEquals($dataSource, $this->model->getDataSourceData()); } + + public function testGetDataSourceDataWithAbstractDataProvider() + { + $requestFieldName = 'request_id'; + $primaryFieldName = 'primary_id'; + $fieldId = 44; + $row = ['key' => 'value', $primaryFieldName => $fieldId]; + $data = [ + 'items' => [$row], + ]; + $dataSource = [ + 'data' => [ + 'general' => $row + ], + ]; + + /** @var DataProviderInterface|\PHPUnit_Framework_MockObject_MockObject $dataProviderMock */ + $dataProviderMock = + $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class) + ->getMock(); + $dataProviderMock->expects($this->once()) + ->method('getRequestFieldName') + ->willReturn($requestFieldName); + $dataProviderMock->expects($this->once()) + ->method('getPrimaryFieldName') + ->willReturn($primaryFieldName); + + $this->contextMock->expects($this->any()) + ->method('getDataProvider') + ->willReturn($dataProviderMock); + $this->contextMock->expects($this->once()) + ->method('getRequestParam') + ->with($requestFieldName) + ->willReturn($fieldId); + + /** @var Filter|\PHPUnit_Framework_MockObject_MockObject $filterMock */ + $filterMock = $this->getMockBuilder(\Magento\Framework\Api\Filter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->filterBuilderMock->expects($this->once()) + ->method('setField') + ->with($primaryFieldName) + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->once()) + ->method('setValue') + ->with($fieldId) + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($filterMock); + + $dataProviderMock->expects($this->once()) + ->method('addFilter') + ->with($filterMock); + $dataProviderMock->expects($this->once()) + ->method('getData') + ->willReturn($data); + + $this->assertEquals($dataSource, $this->model->getDataSourceData()); + } } diff --git a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php index 05b35fb017b4b..2bba8686490b6 100644 --- a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php +++ b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Test\Unit\Controller\Adminhtml\Index; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Escaper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Controller\Adminhtml\Index\Render; use Magento\Ui\Model\UiComponentTypeResolver; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Zend\Http\AbstractMessage; +use Zend\Http\Response; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -97,6 +102,11 @@ class RenderTest extends \PHPUnit\Framework\TestCase */ private $loggerMock; + /** + * @var Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + protected function setUp() { $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) @@ -170,6 +180,10 @@ protected function setUp() $this->uiComponentTypeResolverMock = $this->getMockBuilder(UiComponentTypeResolver::class) ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->createMock(Escaper::class); + $this->escaperMock->expects($this->any()) + ->method('escapeHtml') + ->willReturnArgument(0); $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -181,6 +195,7 @@ protected function setUp() 'contentTypeResolver' => $this->uiComponentTypeResolverMock, 'resultJsonFactory' => $this->resultJsonFactoryMock, 'logger' => $this->loggerMock, + 'escaper' => $this->escaperMock, ] ); } @@ -201,7 +216,7 @@ public function testExecuteAjaxRequestException() ->method('appendBody') ->willThrowException(new \Exception('exception')); - $jsonResultMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $jsonResultMock = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->setMethods(['setData']) ->getMock(); @@ -290,6 +305,34 @@ public function testExecuteAjaxRequestWithoutPermissions(array $dataProviderConf $name = 'test-name'; $renderedData = '<html>data</html>'; + if (false === $isAllowed) { + $jsonResultMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->setMethods(['setStatusHeader', 'setData']) + ->getMock(); + + $jsonResultMock->expects($this->at(0)) + ->method('setStatusHeader') + ->with( + Response::STATUS_CODE_403, + AbstractMessage::VERSION_11, + 'Forbidden' + ) + ->willReturnSelf(); + + $jsonResultMock->expects($this->at(1)) + ->method('setData') + ->with([ + 'error' => 'Forbidden', + 'errorcode' => 403 + ]) + ->willReturnSelf(); + + $this->resultJsonFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($jsonResultMock); + } + $this->requestMock->expects($this->any()) ->method('getParam') ->with('namespace') diff --git a/app/code/Magento/Ui/etc/db_schema.xml b/app/code/Magento/Ui/etc/db_schema.xml index e2a04b0cdc72d..13a384024f18a 100644 --- a/app/code/Magento/Ui/etc/db_schema.xml +++ b/app/code/Magento/Ui/etc/db_schema.xml @@ -18,8 +18,8 @@ comment="Mark current bookmark per user and identifier"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Bookmark title"/> <column xsi:type="longtext" name="config" nullable="true" comment="Bookmark config"/> - <column xsi:type="datetime" name="created_at" on_update="false" nullable="false" comment="Bookmark created at"/> - <column xsi:type="datetime" name="updated_at" on_update="false" nullable="false" comment="Bookmark updated at"/> + <column xsi:type="datetime" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Bookmark created at"/> + <column xsi:type="datetime" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Bookmark updated at"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="bookmark_id"/> </constraint> diff --git a/app/code/Magento/Ui/i18n/de_DE.csv b/app/code/Magento/Ui/i18n/de_DE.csv deleted file mode 100644 index 2efac126b857c..0000000000000 --- a/app/code/Magento/Ui/i18n/de_DE.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/en_US.csv b/app/code/Magento/Ui/i18n/en_US.csv index d51ff98108376..1ce2692886ae3 100644 --- a/app/code/Magento/Ui/i18n/en_US.csv +++ b/app/code/Magento/Ui/i18n/en_US.csv @@ -78,6 +78,7 @@ Keyword,Keyword "Empty Value.","Empty Value." "Please use only letters (a-z or A-Z), numbers (0-9) or spaces only in this field.","Please use only letters (a-z or A-Z), numbers (0-9) or spaces only in this field." "Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.","Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter." +"Attribute code ""%1"" is invalid. Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.","Attribute code ""%1"" is invalid. Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter." "Please use only letters (a-z or A-Z), numbers (0-9), spaces and ""#"" in this field.","Please use only letters (a-z or A-Z), numbers (0-9), spaces and ""#"" in this field." "Please enter a valid phone number. For example (123) 456-7890 or 123-456-7890.","Please enter a valid phone number. For example (123) 456-7890 or 123-456-7890." "Please enter a valid fax number (Ex: 123-456-7890).","Please enter a valid fax number (Ex: 123-456-7890)." @@ -190,4 +191,5 @@ CSV,CSV "Please enter at least {0} characters.","Please enter at least {0} characters." "Please enter a value between {0} and {1} characters long.","Please enter a value between {0} and {1} characters long." "Please enter a value between {0} and {1}.","Please enter a value between {0} and {1}." -"was not uploaded","was not uploaded" \ No newline at end of file +"was not uploaded","was not uploaded" +"The file upload field is disabled.","The file upload field is disabled." diff --git a/app/code/Magento/Ui/i18n/es_ES.csv b/app/code/Magento/Ui/i18n/es_ES.csv deleted file mode 100644 index 2efac126b857c..0000000000000 --- a/app/code/Magento/Ui/i18n/es_ES.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/fr_FR.csv b/app/code/Magento/Ui/i18n/fr_FR.csv deleted file mode 100644 index 2efac126b857c..0000000000000 --- a/app/code/Magento/Ui/i18n/fr_FR.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/nl_NL.csv b/app/code/Magento/Ui/i18n/nl_NL.csv deleted file mode 100644 index 2efac126b857c..0000000000000 --- a/app/code/Magento/Ui/i18n/nl_NL.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/pt_BR.csv b/app/code/Magento/Ui/i18n/pt_BR.csv deleted file mode 100644 index 2efac126b857c..0000000000000 --- a/app/code/Magento/Ui/i18n/pt_BR.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/zh_Hans_CN.csv b/app/code/Magento/Ui/i18n/zh_Hans_CN.csv deleted file mode 100644 index 2efac126b857c..0000000000000 --- a/app/code/Magento/Ui/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/view/base/templates/stepswizard.phtml b/app/code/Magento/Ui/view/base/templates/stepswizard.phtml index 05a537b9a6559..78e73e0cd9a69 100644 --- a/app/code/Magento/Ui/view/base/templates/stepswizard.phtml +++ b/app/code/Magento/Ui/view/base/templates/stepswizard.phtml @@ -13,14 +13,14 @@ <div data-role="steps-wizard-controls" class="steps-wizard-navigation"> <ul class="nav-bar"> - <?php foreach ($block->getSteps() as $step) { ?> + <?php foreach ($block->getSteps() as $step): ?> <li data-role="collapsible" data-bind="css: { 'active': selectedStep() == '<?= /* @escapeNotVerified */ $step->getComponentName() ?>'}"> <a href="#<?= /* @escapeNotVerified */ $step->getComponentName() ?>" data-bind="click: showSpecificStep"> <?= /* @escapeNotVerified */ $step->getCaption() ?> </a> </li> - <?php } ?> + <?php endforeach; ?> </ul> <div class="nav-bar-outer-actions"> <div class="action-wrap" data-role="closeBtn"> @@ -45,13 +45,13 @@ </div> </div> <div data-role="steps-wizard-tab"> - <?php foreach ($block->getSteps() as $step) { ?> + <?php foreach ($block->getSteps() as $step): ?> <div data-bind="visible: selectedStep() == $element.id, css: {'no-display':false}" class="content no-display" id="<?= /* @escapeNotVerified */ $step->getComponentName() ?>" data-role="content"> <?= /* @escapeNotVerified */ $step->getContent() ?> </div> - <?php } ?> + <?php endforeach; ?> </div> </div> diff --git a/app/code/Magento/Ui/view/base/ui_component/etc/definition.map.xml b/app/code/Magento/Ui/view/base/ui_component/etc/definition.map.xml index ccd702c23ea65..8f82b98112f18 100644 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition.map.xml +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition.map.xml @@ -14,6 +14,7 @@ <item name="label" type="string" translate="true" xsi:type="xpath">settings/label</item> <item name="type" type="string" xsi:type="xpath">settings/type</item> <item name="url" type="url" xsi:type="converter">settings/url</item> + <item name="actionDisable" type="boolean" xsi:type="xpath">settings/actionDisable</item> <item name="confirm" xsi:type="array"> <item name="title" type="string" translate="true" xsi:type="xpath">settings/confirm/title</item> <item name="message" type="string" translate="true" xsi:type="xpath">settings/confirm/message</item> diff --git a/app/code/Magento/Ui/view/base/ui_component/etc/definition/action.xsd b/app/code/Magento/Ui/view/base/ui_component/etc/definition/action.xsd index b10ee00818ebc..4dc97910935d6 100644 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition/action.xsd +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition/action.xsd @@ -49,6 +49,13 @@ </xs:documentation> </xs:annotation> </xs:element> + <xs:element name="actionDisable" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Disable and remove this action. + </xs:documentation> + </xs:annotation> + </xs:element> </xs:choice> <xs:attribute name="name" use="required"/> </xs:complexType> @@ -82,6 +89,13 @@ </xs:documentation> </xs:annotation> </xs:element> + <xs:element name="actionDisable" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Disable and remove this action. + </xs:documentation> + </xs:annotation> + </xs:element> <xs:element name="confirm" type="confirmType"> <xs:annotation> <xs:documentation> diff --git a/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd b/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd index cbf69e6046943..ff4d530b5bfd8 100644 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd @@ -476,6 +476,13 @@ </xs:documentation> </xs:annotation> </xs:element> + <xs:element name="aclResource" type="xs:string" minOccurs="0" maxOccurs="1"> + <xs:annotation> + <xs:documentation> + ACL Resource used to validate access to UI Component data + </xs:documentation> + </xs:annotation> + </xs:element> <xs:element ref="param"/> </xs:choice> <xs:attribute name="name" type="xs:string" use="required"> diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js index dee9ba7acc172..583e97b7e9449 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js @@ -15,8 +15,7 @@ define([ ], function (ko, $, _, Element) { 'use strict'; - var transformProp, - isTouchDevice = typeof document.ontouchstart !== 'undefined'; + var transformProp; /** * Get element context @@ -110,11 +109,7 @@ define([ * @param {Object} data - element data */ initListeners: function (elem, data) { - if (isTouchDevice) { - $(elem).on('touchstart', this.mousedownHandler.bind(this, data, elem)); - } else { - $(elem).on('mousedown', this.mousedownHandler.bind(this, data, elem)); - } + $(elem).on('mousedown touchstart', this.mousedownHandler.bind(this, data, elem)); }, /** @@ -131,26 +126,20 @@ define([ $table = $(elem).parents('table').eq(0), $tableWrapper = $table.parent(); + this.disableScroll(); $(recordNode).addClass(this.draggableElementClass); $(originRecord).addClass(this.draggableElementClass); this.step = this.step === 'auto' ? originRecord.height() / 2 : this.step; drEl.originRow = originRecord; drEl.instance = recordNode = this.processingStyles(recordNode, elem); drEl.instanceCtx = this.getRecord(originRecord[0]); - drEl.eventMousedownY = isTouchDevice ? event.originalEvent.touches[0].pageY : event.pageY; + drEl.eventMousedownY = this.getPageY(event); drEl.minYpos = $table.offset().top - originRecord.offset().top + $table.children('thead').outerHeight(); drEl.maxYpos = drEl.minYpos + $table.children('tbody').outerHeight() - originRecord.outerHeight(); $tableWrapper.append(recordNode); - - if (isTouchDevice) { - this.body.bind('touchmove', this.mousemoveHandler); - this.body.bind('touchend', this.mouseupHandler); - } else { - this.body.bind('mousemove', this.mousemoveHandler); - this.body.bind('mouseup', this.mouseupHandler); - } - + this.body.bind('mousemove touchmove', this.mousemoveHandler); + this.body.bind('mouseup touchend', this.mouseupHandler); }, /** @@ -160,16 +149,13 @@ define([ */ mousemoveHandler: function (event) { var depEl = this.draggableElement, - pageY = isTouchDevice ? event.originalEvent.touches[0].pageY : event.pageY, + pageY = this.getPageY(event), positionY = pageY - depEl.eventMousedownY, processingPositionY = positionY + 'px', processingMaxYpos = depEl.maxYpos + 'px', processingMinYpos = depEl.minYpos + 'px', depElement = this.getDepElement(depEl.instance, positionY, depEl.originRow); - event.stopPropagation(); - event.preventDefault(); - if (depElement) { depEl.depElement ? depEl.depElement.elem.removeClass(depEl.depElement.className) : false; depEl.depElement = depElement; @@ -194,9 +180,10 @@ define([ mouseupHandler: function (event) { var depElementCtx, drEl = this.draggableElement, - pageY = isTouchDevice ? event.originalEvent.touches[0].pageY : event.pageY, + pageY = this.getPageY(event), positionY = pageY - drEl.eventMousedownY; + this.enableScroll(); drEl.depElement = this.getDepElement(drEl.instance, positionY, this.draggableElement.originRow); drEl.instance.remove(); @@ -212,13 +199,8 @@ define([ drEl.originRow.removeClass(this.draggableElementClass); - if (isTouchDevice) { - this.body.unbind('touchmove', this.mousemoveHandler); - this.body.unbind('touchend', this.mouseupHandler); - } else { - this.body.unbind('mousemove', this.mousemoveHandler); - this.body.unbind('mouseup', this.mouseupHandler); - } + this.body.unbind('mousemove touchmove', this.mousemoveHandler); + this.body.unbind('mouseup touchend', this.mouseupHandler); this.draggableElement = {}; }, @@ -402,6 +384,55 @@ define([ index = _.isFunction(ctx.$index) ? ctx.$index() : ctx.$index; return this.recordsCache()[index]; + }, + + /** + * Get correct page Y + * + * @param {Object} event - current event + * @returns {integer} + */ + getPageY: function (event) { + var pageY; + + if (event.type.indexOf('touch') >= 0) { + if (event.originalEvent.touches[0]) { + pageY = event.originalEvent.touches[0].pageY; + } else { + pageY = event.originalEvent.changedTouches[0].pageY; + } + } else { + pageY = event.pageY; + } + + return pageY; + }, + + /** + * Disable page scrolling + */ + disableScroll: function () { + document.body.addEventListener('touchmove', this.preventDefault, { + passive: false + }); + }, + + /** + * Enable page scrolling + */ + enableScroll: function () { + document.body.removeEventListener('touchmove', this.preventDefault, { + passive: false + }); + }, + + /** + * Prevent default function + * + * @param {Object} event - event object + */ + preventDefault: function (event) { + event.preventDefault(); } }); diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js index dc6f8d930a144..17b2d1db4eb1b 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js @@ -33,6 +33,15 @@ define([ } }, + /** + * @inheritdoc + */ + initialize: function () { + this.setToInsertData = _.debounce(this.setToInsertData, 200); + + return this._super(); + }, + /** * Calls 'initObservable' of parent * diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js index 1d52fc78d7a85..cbbfbdb127ad7 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js @@ -543,6 +543,7 @@ define([ data = this.createHeaderTemplate(cell.config); cell.config.labelVisible = false; _.extend(data, { + defaultLabelVisible: data.visible(), label: cell.config.label, name: cell.name, required: !!cell.config.validation, diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js index 54309ca068513..9a9d478904775 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js @@ -25,7 +25,7 @@ define([ }, listens: { position: 'initPosition', - elems: 'setColumnVisibileListener' + elems: 'setColumnVisibleListener' }, links: { position: '${ $.name }.${ $.positionProvider }:value' @@ -123,7 +123,7 @@ define([ /** * Set column visibility listener */ - setColumnVisibileListener: function () { + setColumnVisibleListener: function () { var elem = _.find(this.elems(), function (curElem) { return !curElem.hasOwnProperty('visibleListener'); }); @@ -245,7 +245,7 @@ define([ label = _.findWhere(this.parentComponent().labels(), { name: index }); - label.visible() !== state ? label.visible(state) : false; + label.defaultLabelVisible && label.visible(state); } else { elems[curElem].visible(state); } diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/button.js b/app/code/Magento/Ui/view/base/web/js/form/components/button.js index df85af5824d92..bfc2eb2b8852b 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/button.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/button.js @@ -45,7 +45,8 @@ define([ .observe([ 'visible', 'disabled', - 'title' + 'title', + 'childError' ]); }, diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js b/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js index 1ddc4dc247b24..9ee387e0e6a7c 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js @@ -162,6 +162,10 @@ define([ } this.error(hasErrors || message); + + if (hasErrors || message) { + this.open(); + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/insert.js b/app/code/Magento/Ui/view/base/web/js/form/components/insert.js index 26ad9fbfec013..d8cbcc9cc1732 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/insert.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/insert.js @@ -237,10 +237,21 @@ define([ * @param {*} data */ onRender: function (data) { + var resp; + this.loading(false); - this.set('content', data); - this.isRendered = true; - this.startRender = false; + + try { + resp = JSON.parse(data); + + if (resp.ajaxExpired) { + window.location.href = resp.ajaxRedirect; + } + } catch (e) { + this.set('content', data); + this.isRendered = true; + this.startRender = false; + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js index 3b98d2c93c7a9..ca3d383accca1 100755 --- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js @@ -408,6 +408,7 @@ define([ isValid = this.disabled() || !this.visible() || result.passed; this.error(message); + this.error.valueHasMutated(); this.bubble('error', message); //TODO: Implement proper result propagation for form 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 a5eb7d5d1f570..4e532c9d48cc6 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 @@ -122,10 +122,12 @@ define([ shiftedValue = moment.tz(value, 'UTC').tz(this.storeTimeZone); } else { dateFormat = this.shiftedValue() ? this.outputDateFormat : this.inputDateFormat; - shiftedValue = moment(value, dateFormat); } + if (!shiftedValue.isValid()) { + shiftedValue = moment(value, this.inputDateFormat); + } shiftedValue = shiftedValue.format(this.pickerDateTimeFormat); } else { shiftedValue = ''; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 357571350a268..f28569caa0053 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -16,7 +16,8 @@ define([ 'Magento_Ui/js/form/element/abstract', 'mage/backend/notification', 'mage/translate', - 'jquery/file-uploader' + 'jquery/file-uploader', + 'mage/adminhtml/tools' ], function ($, _, utils, uiAlert, validator, Element, notification, $t) { 'use strict'; @@ -348,6 +349,12 @@ define([ allowed = this.isFileAllowed(file), target = $(e.target); + if (this.disabled()) { + this.notifyError($t('The file upload field is disabled.')); + + return; + } + if (allowed.passed) { target.on('fileuploadsend', function (event, postData) { postData.data.append('param_name', this.paramName); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js index 0ae09f14fa946..b490ac557e71b 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js @@ -11,8 +11,7 @@ define([ 'Magento_Ui/js/modal/alert', 'Magento_Ui/js/lib/validation/validator', 'Magento_Ui/js/form/element/file-uploader', - 'mage/adminhtml/browser', - 'mage/adminhtml/tools' + 'mage/adminhtml/browser' ], function ($, _, utils, uiAlert, validator, Element, browser) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js index 911574a0fb438..0eaacdc32567b 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js @@ -20,13 +20,33 @@ define([ } }, + /** + * Initializes observable properties of instance + * + * @returns {Abstract} Chainable. + */ + initObservable: function () { + this._super(); + + /** + * equalityComparer function + * + * @returns boolean. + */ + this.value.equalityComparer = function (oldValue, newValue) { + return !oldValue && !newValue || oldValue === newValue; + }; + + return this; + }, + /** * @param {String} value */ update: function (value) { var country = registry.get(this.parentName + '.' + 'country_id'), options = country.indexedOptions, - option; + option = null; if (!value) { return; @@ -34,6 +54,10 @@ define([ option = options[value]; + if (!option) { + return; + } + if (option['is_zipcode_optional']) { this.error(false); this.validation = _.omit(this.validation, 'required-entry'); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js index dba0992c5ba52..4479cff5135dc 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js @@ -131,6 +131,7 @@ define([ return Abstract.extend({ defaults: { options: [], + total: 0, listVisible: false, value: [], filterOptions: false, @@ -153,6 +154,7 @@ define([ labelsDecoration: false, disableLabel: false, filterRateLimit: 500, + filterRateLimitMethod: 'notifyAtFixedRate', closeBtnLabel: $t('Done'), optgroupTmpl: 'ui/grid/filters/elements/ui-select-optgroup', quantityPlaceholder: $t('options'), @@ -180,6 +182,7 @@ define([ debounce: 300, missingValuePlaceholder: $t('Entity with ID: %s doesn\'t exist'), isDisplayMissingValuePlaceholder: false, + currentSearchKey: '', listens: { listVisible: 'cleanHoveredElement', filterInputValue: 'filterOptionsList', @@ -330,7 +333,10 @@ define([ ]); this.filterInputValue.extend({ - rateLimit: this.filterRateLimit + rateLimit: { + timeout: this.filterRateLimit, + method: this.filterRateLimitMethod + } }); return this; @@ -460,7 +466,7 @@ define([ } if (this.searchOptions) { - return _.debounce(this.loadOptions.bind(this, value), this.debounce)(); + return this.loadOptions(value); } this.cleanHoveredElement(); @@ -547,11 +553,21 @@ define([ _setItemsQuantity: function (data) { if (this.showFilteredQuantity) { data || parseInt(data, 10) === 0 ? - this.itemsQuantity(data + ' ' + this.quantityPlaceholder) : + this.itemsQuantity(this.getItemsPlaceholder(data)) : this.itemsQuantity(''); } }, + /** + * Return formatted items placeholder. + * + * @param {Object} data - option data + * @returns {String} + */ + getItemsPlaceholder: function (data) { + return data + ' ' + this.quantityPlaceholder; + }, + /** * Remove element from selected array */ @@ -1234,13 +1250,11 @@ define([ * @param {Number} page */ processRequest: function (searchKey, page) { - var total = 0, - existingOptions = this.options(); - this.loading(true); + this.currentSearchKey = searchKey; $.ajax({ url: this.searchUrl, - type: 'post', + type: 'get', dataType: 'json', context: this, data: { @@ -1248,27 +1262,39 @@ define([ page: page, limit: this.pageLimit }, + success: $.proxy(this.success, this), + error: $.proxy(this.error, this), + beforeSend: $.proxy(this.beforeSend, this), + complete: $.proxy(this.complete, this, searchKey, page) + }); + }, - /** @param {Object} response */ - success: function (response) { - _.each(response.options, function (opt) { - existingOptions.push(opt); - }); - total = response.total; - this.options(existingOptions); - }, - - /** set empty array if error occurs */ - error: function () { - this.options([]); - }, + /** @param {Object} response */ + success: function (response) { + var existingOptions = this.options(); - /** cache options and stop loading*/ - complete: function () { - this.setCachedSearchResults(searchKey, this.options(), page, total); - this.afterLoadOptions(searchKey, page, total); - } + _.each(response.options, function (opt) { + existingOptions.push(opt); }); + + this.total = response.total; + this.options(existingOptions); + }, + + /** add actions before ajax request */ + beforeSend: function () { + + }, + + /** set empty array if error occurs */ + error: function () { + this.options([]); + }, + + /** cache options and stop loading*/ + complete: function (searchKey, page) { + this.setCachedSearchResults(searchKey, this.options(), page, this.total); + this.afterLoadOptions(searchKey, page, this.total); }, /** @@ -1279,9 +1305,9 @@ define([ * @param {Number} total */ afterLoadOptions: function (searchKey, page, total) { - this._setItemsQuantity(total); - this.lastSearchPage = page; this.lastSearchKey = searchKey; + this.lastSearchPage = page; + this._setItemsQuantity(total); this.loading(false); } }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js index 3d02afcc40a9e..ce19899cd12cd 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js @@ -18,6 +18,7 @@ define([ 'use strict'; return Abstract.extend({ + currentWysiwyg: undefined, defaults: { elementSelector: 'textarea', suffixRegExpPattern: '${ $.wysiwygUniqueSuffix }', @@ -53,6 +54,10 @@ define([ // disable editor completely after initialization is field is disabled varienGlobalEvents.attachEventHandler('wysiwygEditorInitialized', function () { + if (!_.isUndefined(window.tinyMceEditors)) { + this.currentWysiwyg = window.tinyMceEditors[this.wysiwygId]; + } + if (this.disabled()) { this.setDisabled(true); } @@ -136,14 +141,9 @@ define([ } /* eslint-disable no-undef */ - if (typeof wysiwyg !== 'undefined' && wysiwyg.activeEditor()) { - if (wysiwyg && disabled) { - wysiwyg.setEnabledStatus(false); - wysiwyg.getPluginButtons().prop('disabled', 'disabled'); - } else if (wysiwyg) { - wysiwyg.setEnabledStatus(true); - wysiwyg.getPluginButtons().removeProp('disabled'); - } + if (!_.isUndefined(this.currentWysiwyg) && this.currentWysiwyg.activeEditor()) { + this.currentWysiwyg.setEnabledStatus(!disabled); + this.currentWysiwyg.getPluginButtons().prop('disabled', disabled); } } }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js index 076465351cded..cfcd37a65b8c1 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js @@ -382,7 +382,7 @@ define([ * Checks if specified view is in editing state. * * @param {String} index - Index of a view to be checked. - * @returns {Bollean} + * @returns {Boolean} */ isEditing: function (index) { return this.editing === index; diff --git a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js index dad67da3ea8ad..547cdab16cdf1 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js @@ -199,7 +199,7 @@ define([ }, /** - * Caches requests object with provdided parameters + * Caches requests object with provided parameters * and data object associated with it. * * @param {Object} data - Data associated with request. diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js index f68a6f97d964f..ca82ff81d3b6f 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js @@ -54,7 +54,7 @@ define([ /** * Proxy save method which might invoke - * data valiation prior to its' saving. + * data validation prior to its' saving. * * @param {Object} data - Data to be processed. * @returns {jQueryPromise} @@ -128,7 +128,7 @@ define([ /** * Handles ajax success callback. * - * @param {jQueryPromise} promise - Promise to be resoloved. + * @param {jQueryPromise} promise - Promise to be resolved. * @param {*} data - See 'jquery' ajax success callback. */ onSuccess: function (promise, data) { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js index a4785aea03743..ece49cc8fe27c 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js @@ -357,7 +357,7 @@ define([ /** * Resets specific records' data - * to the data present in asscotiated row. + * to the data present in associated row. * * @param {(Number|String)} id - See 'getId' method. * @param {Boolean} [isIndex=false] - See 'getId' method. @@ -403,7 +403,7 @@ define([ /** * Disables editing of specified fields. * - * @param {Array} fields - An array of fields indeces to be disabled. + * @param {Array} fields - An array of fields indexes to be disabled. * @returns {Editor} Chainable. */ disableFields: function (fields) { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js index aa83083cac3c9..9b8998368c5ff 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js @@ -264,7 +264,7 @@ define([ /** * Validates all of the available fields. * - * @returns {Array} An array with validatation results. + * @returns {Array} An array with validation results. */ validate: function () { return this.elems.map('validate'); @@ -280,7 +280,7 @@ define([ }, /** - * Counts total errors ammount accros all fields. + * Counts total errors amount across all fields. * * @returns {Number} */ @@ -306,7 +306,7 @@ define([ }, /** - * Updates 'fields' array filling it with available edtiors + * Updates 'fields' array filling it with available editors * or with column instances if associated field is not present. * * @returns {Record} Chainable. 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 abd79e797e413..8e6f1496495c7 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 @@ -19,7 +19,9 @@ define([ defaults: { template: 'ui/grid/paging/paging', totalTmpl: 'ui/grid/paging-total', + totalRecords: 0, pageSize: 20, + pages: 1, current: 1, selectProvider: 'ns = ${ $.ns }, index = ids', @@ -35,7 +37,8 @@ define([ imports: { pageSize: '${ $.sizesConfig.name }:value', totalSelected: '${ $.selectProvider }:totalSelected', - totalRecords: '${ $.provider }:data.totalRecords' + totalRecords: '${ $.provider }:data.totalRecords', + filters: '${ $.provider }:params.filters' }, exports: { @@ -43,6 +46,11 @@ define([ current: '${ $.provider }:params.paging.current' }, + statefull: { + pageSize: true, + current: true + }, + listens: { 'pages': 'onPagesChange', 'pageSize': 'onPageSizeChange', @@ -173,7 +181,9 @@ define([ * @returns {Paging} Chainable. */ goFirst: function () { - this.current = 1; + if (!_.isUndefined(this.filters)) { + this.current = 1; + } return this; }, @@ -219,13 +229,11 @@ define([ /** * Calculates new page cursor based on the * previous and current page size values. - * - * @returns {Number} Updated cursor value. */ updateCursor: function () { var cursor = this.current - 1, size = this.pageSize, - oldSize = this.previousSize, + oldSize = _.isUndefined(this.previousSize) ? this.pageSize : this.previousSize, delta = cursor * (oldSize - size) / size; delta = size > oldSize ? 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 19536e7ff8c18..ce53b23b79e11 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 @@ -11,14 +11,15 @@ define([ 'uiLayout', 'mage/translate', 'mageUtils', - 'uiElement' -], function (_, layout, $t, utils, Element) { + 'uiElement', + 'jquery' +], function (_, layout, $t, utils, Element, $) { 'use strict'; return Element.extend({ defaults: { template: 'ui/grid/search/search', - placeholder: $t('Search by keyword'), + placeholder: 'Search by keyword', label: $t('Keyword'), value: '', previews: [], @@ -29,11 +30,13 @@ define([ tracks: { value: true, previews: true, - inputValue: true + inputValue: true, + focused: true }, imports: { inputValue: 'value', - updatePreview: 'value' + updatePreview: 'value', + focused: false }, exports: { value: '${ $.provider }:params.search' @@ -88,6 +91,18 @@ define([ return this; }, + /** + * Click To ScrollTop. + */ + scrollTo: function ($data) { + $('html, body').animate({ + scrollTop: 0 + }, 'slow', function () { + $data.focused = false; + $data.focused = true; + }); + }, + /** * Resets input value to the last applied state. * diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js b/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js index eb4f2b39128e2..0491390d2b6c2 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js @@ -310,7 +310,7 @@ define([ * @private * * @param {Array} args - An array of arguments to pass to the next delegation call. - * @returns {Array} An array of delegation resutls. + * @returns {Array} An array of delegation results. */ _delegate: function (args) { var result; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js b/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js index 87b5b2f5fe8fe..adeb510ab3e40 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js @@ -79,7 +79,7 @@ define([ /** * Extracts and parses data stored in localStorage by the - * key specified in 'root' varaible. + * key specified in 'root' variable. * * @returns {Object} */ @@ -114,8 +114,8 @@ define([ * * @param {String} path - Path to the property. * - * @example Retrieveing data. - * localStoarge => + * @example Retrieving data. + * localStorage => * 'appData' => ' * "one": {"two": "three"} * ' @@ -139,7 +139,7 @@ define([ * * @example Setting data. * storage.set('one.two', 'four'); - * => localStoarge => + * => localStorage => * 'appData' => ' * "one": {"two": "four"} * ' @@ -159,7 +159,7 @@ define([ * * @example Removing data. * storage.remove('one.two', 'four'); - * => localStoarge => + * => localStorage => * 'appData' => ' * "one": {} * ' diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js index 2e0c53373f807..2cfd961619249 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js @@ -519,7 +519,7 @@ define([ }, /** - * Custom 'render' attrobute handler function. Wraps child elements + * Custom 'render' attribute handler function. Wraps child elements * of a node with knockout's 'ko template:' comment tag. * * @param {HTMLElement} node - Element to be processed. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js b/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js index fe83f600132ed..bf7ae0cdc3e98 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js @@ -42,14 +42,14 @@ define([], function () { * Method that creates object of messages * @param {String} requested - log message that showing that request for class is started * @param {String} loaded - log message that show when requested class is loaded - * @param {String} failded - log message that show when requested class is failed + * @param {String} failed - log message that show when requested class is failed * @returns {Object} */ - LogUtils.prototype.createMessages = function (requested, loaded, failded) { + LogUtils.prototype.createMessages = function (requested, loaded, failed) { return { requested: requested || '', loaded: loaded || '', - failed: failded || '' + failed: failed || '' }; }; @@ -57,14 +57,14 @@ define([], function () { * Method that creates object of log levels * @param {String} requested - log message that showing that request for class is started * @param {String} loaded - log message that show when requested class is loaded - * @param {String} failded - log message that show when requested class is failed + * @param {String} failed - log message that show when requested class is failed * @returns {Object} */ - LogUtils.prototype.createLevels = function (requested, loaded, failded) { + LogUtils.prototype.createLevels = function (requested, loaded, failed) { return { requested: requested || 'info', loaded: loaded || 'info', - failed: failded || 'warn' + failed: failed || 'warn' }; }; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index d765f842a0895..97b47f77beeab 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -920,12 +920,12 @@ define([ ], 'validate-per-page-value-list': [ function (value) { - var isValid = utils.isEmpty(value), + var isValid = true, values = value.split(','), i; - if (isValid) { - return true; + if (utils.isEmpty(value)) { + return isValid; } for (i = 0; i < values.length; i++) { diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js index be8fd2ce9fcef..407984c7881a2 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js @@ -48,6 +48,10 @@ define([ params : [params]; + if (typeof message === 'function') { + message = message.call(rule); + } + message = params.reduce(function (msg, param, idx) { return msg.replace(new RegExp('\\{' + idx + '\\}', 'g'), param); }, message); @@ -60,7 +64,7 @@ define([ } /** - * Validates provied value by a specified set of rules. + * Validates provided value by a specified set of rules. * * @param {(String|Object)} rules - One or many validation rules. * @param {*} value - Value to be checked. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/bindings.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/bindings.js index 77de8a1ceb0ed..48515b668f80d 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/bindings.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/bindings.js @@ -89,7 +89,7 @@ define([ /** * Adds specified bindings to each DOM element in - * collection and evalutes them with provided context. + * collection and evaluates them with provided context. * * @param {(Object|Function)} data - Either bindings object or a function * which returns bindings data for each element in collection. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js index 03012918f4a0d..f8e752fb77af2 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js @@ -141,7 +141,7 @@ define([ } /** - * Calls handlers assocoiated with an added node. + * Calls handlers associated with an added node. * Adds listeners for the node removal. * * @param {HTMLElement} node - Added node. @@ -163,7 +163,7 @@ define([ } /** - * Calls handlers assocoiated with a removed node. + * Calls handlers associated with a removed node. * * @param {HTMLElement} node - Removed node. */ diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html index 1a21e1b2f1c71..6da4f82fa8b9e 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html @@ -41,7 +41,7 @@ <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> <td if="elem.template" css="$parent.setClasses(elem)" - visible="elem.visible" + visible="elem.visible() && elem.formElement !== 'hidden'" disable="elem.disabled" template="elem.template"/> <!-- /ko --> diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html index d0b12549bd66d..e5d73a62b329e 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html @@ -58,7 +58,7 @@ <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> <td if="elem.template" - visible="elem.visible" + visible="elem.visible() && elem.formElement !== 'hidden'" disable="elem.disabled" css="$parent.setClasses(elem)" template="elem.template" diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/button.html b/app/code/Magento/Ui/view/base/web/templates/form/element/button.html index a8b6c3a858956..766df35f5764b 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/element/button.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/button.html @@ -11,3 +11,15 @@ attr="'data-index': index"> <span text="title"/> </button> + +<if args="childError"> + <strong class="_error"> + <span class="admin__page-nav-item-messages"> + <span class="admin__page-nav-item-message _error"> + <span class="admin__page-nav-item-message-icon"></span> + <span class="admin__page-nav-item-message-tooltip" + data-bind="i18n: 'This element contains invalid data. Please resolve this before saving.'">This element contains invalid data. Please resolve this before saving.</span> + </span> + </span> + </strong> +</if> diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html b/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html index a92b85cb47401..cf4e2243b5886 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/uploader/uploader.html @@ -6,7 +6,7 @@ --> <div class="admin__field" visible="visible" css="$data.additionalClasses"> - <label class="admin__field-label" if="$data.label" attr="for: uid"> + <label class="admin__field-label" if="$data.label" attr="for: uid" visible="$data.labelVisible"> <span translate="label" attr="'data-config-scope': $data.scopeLabel"/> </label> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html index 3ef64fd4b5371..36a3232c3e61a 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html @@ -6,7 +6,7 @@ --> <div class="admin__action-dropdown-wrap admin__data-grid-action-bookmarks" collapsible> <button class="admin__action-dropdown" type="button" toggleCollapsible> - <span class="admin__action-dropdown-text" text="activeView.label"/> + <span class="admin__action-dropdown-text" translate="activeView.label"/> </button> <ul class="admin__action-dropdown-menu"> <repeat args="foreach: viewsArray, item: '$view'"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html index b52669e2cd28d..521ce9fc806ac 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html @@ -30,7 +30,7 @@ </div> <div class="action-dropdown-menu-item"> - <a href="" class="action-dropdown-menu-link" text="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> + <a href="" class="action-dropdown-menu-link" translate="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> <div class="action-dropdown-menu-item-actions" if="$view().editable"> <button class="action-edit" type="button" attr="title: $t('Edit bookmark')" click="editView.bind($data, $view().index)"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html index 56244422a6b43..1ad0e7505ec9d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html @@ -19,7 +19,7 @@ css: { _selected: $parent.root.isSelected(option.value), _hover: $parent.root.isHovered(option, $element), - _expended: $parent.root.getLevelVisibility($data), + _expended: $parent.root.getLevelVisibility($data) || $data.visible, _unclickable: $parent.root.isLabelDecoration($data), _last: $parent.root.addLastElement($data), '_with-checkbox': $parent.root.showCheckbox diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html index bf3e2df8a82d0..b9425c020c0e9 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html @@ -36,7 +36,7 @@ class="action-select admin__action-multiselect" data-role="advanced-select" data-bind=" - css: {_active: multiselectFocus}, + css: {_active: listVisible}, click: function(data, event) { toggleListVisible(data, event) } @@ -73,7 +73,7 @@ class="action-select admin__action-multiselect" data-role="advanced-select" data-bind=" - css: {_active: multiselectFocus}, + css: {_active: listVisible}, click: function(data, event) { toggleListVisible(data, event) } @@ -160,7 +160,7 @@ css: { _selected: $parent.isSelectedValue(option), _hover: $parent.isHovered(option, $element), - _expended: $parent.getLevelVisibility($data), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), _unclickable: $parent.isLabelDecoration($data), _last: $parent.addLastElement($data), '_with-checkbox': $parent.showCheckbox @@ -174,6 +174,7 @@ <div class="admin__action-multiselect-dropdown" data-bind=" click: function(event){ + $parent.showLevels($data); $parent.openChildLevel($data, $element, event); }, clickBubble: false diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html index 39d996e05c3a6..fcad729a95fbb 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html @@ -5,16 +5,18 @@ */ --> <div class="data-grid-search-control-wrap"> - <label class="data-grid-search-label" attr="title: $t('Search'), for: index"> + <label class="data-grid-search-label" attr="title: $t('Search'), for: index" data-bind="click: scrollTo"> <span translate="'Search'"/> </label> <input class="admin__control-text data-grid-search-control" type="text" data-bind=" + i18n: placeholder, attr: { id: index, - placeholder: placeholder + placeholder: $t(placeholder) }, textInput: inputValue, + hasFocus: focused, keyboard: { 13: apply.bind($data, false), 27: cancel diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html index c5d87a4b16c4e..610d78e00b81d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html @@ -6,7 +6,7 @@ --> <ul class="action-submenu" each="data: action.actions, as: 'action'" css="_active: action.visible"> <li css="_visible: $data.visible"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html index 1aeb48b7c7698..d11d4aa243737 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html @@ -11,7 +11,7 @@ <div class="action-menu-items"> <ul class="action-menu" each="data: actions, as: 'action'" css="_active: opened"> <li css="_visible: $data.visible, _parent: $data.actions"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html index 3d4f9eefe5afa..f8d1cbbcad5c1 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html @@ -13,14 +13,20 @@ data-bind="attr: {href: tooltip.link}, mageInit: {'dropdown':{'activeClass': '_active'}}"></a> <!-- /ko --> + <span id="tooltip-label" class="label"><!-- ko i18n: 'Tooltip' --><!-- /ko --></span> <!-- ko if: (!tooltip.link)--> - <span class="field-tooltip-action action-help" - tabindex="0" - data-toggle="dropdown" - data-bind="mageInit: {'dropdown':{'activeClass': '_active'}}"></span> + <span + id="tooltip" + class="field-tooltip-action action-help" + tabindex="0" + data-toggle="dropdown" + data-bind="mageInit: {'dropdown':{'activeClass': '_active', 'parent': '.field-tooltip.toggle'}}" + aria-labelledby="tooltip-label" + > + </span> <!-- /ko --> - <div class="field-tooltip-content" + <div class="field-tooltip-content" data-target="dropdown" translate="tooltip.description"> </div> </div> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html new file mode 100644 index 0000000000000..226ad2915bb61 --- /dev/null +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html @@ -0,0 +1,37 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="field-control" css="'_with-tooltip': $data.tooltip"> + <div class="file-uploader" data-role="drop-zone" css="_loading: isLoading"> + <div class="file-uploader-area"> + <input type="file" afterRender="onElementRender" attr="id: uid, name: inputName, multiple: isMultipleFiles" + disable="disabled"/> + <label class="file-uploader-button action-default" attr="for: uid" translate="'Upload'"/> + + <span class="file-uploader-spinner"/> + <render args="fallbackResetTpl" if="$data.showFallbackReset && $data.isDifferedFromDefault"/> + </div> + + <render args="tooltipTpl" if="$data.tooltip"/> + + <div class="field-note" if="$data.notice" attr="id: noticeId"> + <span html="notice"/> + </div> + + <each args="data: value, as: '$file'" render="$parent.getPreviewTmpl($file)"/> + + <div if="isMultipleFiles" class="file-uploader-summary"> + <label attr="for: uid" + class="file-uploader-placeholder" + css="'placeholder-' + placeholderType"> + <span class="file-uploader-placeholder-text" + translate="'Click here or drag and drop to add files.'"/> + </label> + </div> + </div> + <render args="$data.service.template" if="$data.hasService()"/> +</div> diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index 06f68db05398f..0e2ce05f2d079 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Ups\Model; @@ -77,7 +78,7 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface * * @var string */ - protected $_defaultCgiGatewayUrl = 'http://www.ups.com:80/using/services/rave/qcostcgi.cgi'; + protected $_defaultCgiGatewayUrl = 'https://www.ups.com/using/services/rave/qcostcgi.cgi'; /** * Test urls for shipment @@ -332,6 +333,14 @@ public function setRequest(RateRequest $request) $destCountry = self::GUAM_COUNTRY_ID; } + // For UPS, Las Palmas and Santa Cruz de Tenerife will be represented by Canary Islands country + if ($destCountry === 'ES' && + ($request->getDestRegionCode() === 'Las Palmas' + || $request->getDestRegionCode() === 'Santa Cruz de Tenerife') + ) { + $destCountry = 'IC'; + } + $country = $this->_countryFactory->create()->load($destCountry); $rowRequest->setDestCountry($country->getData('iso2_code') ?: $destCountry); @@ -446,7 +455,7 @@ protected function _getCgiQuotes() { $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr($rowRequest->getDestPostal(), 0, 5); + $destPostal = substr((string)$rowRequest->getDestPostal(), 0, 5); } else { $destPostal = $rowRequest->getDestPostal(); } @@ -464,7 +473,7 @@ protected function _getCgiQuotes() '47_rate_chart' => $rowRequest->getPickup(), '48_container' => $rowRequest->getContainer(), '49_residential' => $rowRequest->getDestType(), - 'weight_std' => strtolower($rowRequest->getUnitMeasure()), + 'weight_std' => strtolower((string)$rowRequest->getUnitMeasure()), ]; $params['47_rate_chart'] = $params['47_rate_chart']['label']; @@ -528,7 +537,7 @@ protected function _parseCgiResponse($response) $priceArr = []; if (strlen(trim($response)) > 0) { $rRows = explode("\n", $response); - $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); + $allowedMethods = explode(",", (string)$this->getConfigData('allowed_methods')); foreach ($rRows as $rRow) { $row = explode('%', $rRow); switch (substr($row[0], -1)) { @@ -604,7 +613,7 @@ protected function _getXmlQuotes() $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr($rowRequest->getDestPostal(), 0, 5); + $destPostal = substr((string)$rowRequest->getDestPostal(), 0, 5); } else { $destPostal = $rowRequest->getDestPostal(); } @@ -824,76 +833,15 @@ protected function _parseXmlResponse($xmlResponse) $allowedCurrencies = $this->_currencyFactory->create()->getConfigAllowCurrencies(); foreach ($arr as $shipElement) { - $code = (string)$shipElement->Service->Code; - if (in_array($code, $allowedMethods)) { - //The location of tax information is in a different place - // depending on whether we are using negotiated rates or not - if ($negotiatedActive) { - $includeTaxesArr = $xml->getXpath( - "//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates" - . "/NetSummaryCharges/TotalChargesWithTaxes" - ); - $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); - if ($includeTaxesActive) { - $cost = $shipElement->NegotiatedRates - ->NetSummaryCharges - ->TotalChargesWithTaxes - ->MonetaryValue; - - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates - ->NetSummaryCharges - ->TotalChargesWithTaxes - ->CurrencyCode - ); - } else { - $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode - ); - } - } else { - $includeTaxesArr = $xml->getXpath( - "//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes" - ); - $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); - if ($includeTaxesActive) { - $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalChargesWithTaxes->CurrencyCode - ); - } else { - $cost = $shipElement->TotalCharges->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalCharges->CurrencyCode - ); - } - } - - //convert price with Origin country currency code to base currency code - $successConversion = true; - if ($responseCurrencyCode) { - if (in_array($responseCurrencyCode, $allowedCurrencies)) { - $cost = (double)$cost * $this->_getBaseCurrencyRate($responseCurrencyCode); - } else { - $errorTitle = __( - 'We can\'t convert a rate from "%1-%2".', - $responseCurrencyCode, - $this->_request->getPackageCurrency()->getCode() - ); - $error = $this->_rateErrorFactory->create(); - $error->setCarrier('ups'); - $error->setCarrierTitle($this->getConfigData('title')); - $error->setErrorMessage($errorTitle); - $successConversion = false; - } - } - - if ($successConversion) { - $costArr[$code] = $cost; - $priceArr[$code] = $this->getMethodPrice((float)$cost, $code); - } - } + $this->processShippingRateForItem( + $shipElement, + $allowedMethods, + $allowedCurrencies, + $costArr, + $priceArr, + $negotiatedActive, + $xml + ); } } else { $arr = $xml->getXpath("//RatingServiceSelectionResponse/Response/Error/ErrorDescription/text()"); @@ -936,6 +884,99 @@ protected function _parseXmlResponse($xmlResponse) return $result; } + /** + * Processing rate for ship element + * + * @param \Magento\Framework\Simplexml\Element $shipElement + * @param array $allowedMethods + * @param array $allowedCurrencies + * @param array $costArr + * @param array $priceArr + * @param bool $negotiatedActive + * @param \Magento\Framework\Simplexml\Config $xml + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function processShippingRateForItem( + \Magento\Framework\Simplexml\Element $shipElement, + array $allowedMethods, + array $allowedCurrencies, + array &$costArr, + array &$priceArr, + bool $negotiatedActive, + \Magento\Framework\Simplexml\Config $xml + ): void { + $code = (string)$shipElement->Service->Code; + if (in_array($code, $allowedMethods)) { + //The location of tax information is in a different place + // depending on whether we are using negotiated rates or not + if ($negotiatedActive) { + $includeTaxesArr = $xml->getXpath( + "//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates" + . "/NetSummaryCharges/TotalChargesWithTaxes" + ); + $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); + if ($includeTaxesActive) { + $cost = $shipElement->NegotiatedRates + ->NetSummaryCharges + ->TotalChargesWithTaxes + ->MonetaryValue; + + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->NegotiatedRates + ->NetSummaryCharges + ->TotalChargesWithTaxes + ->CurrencyCode + ); + } else { + $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode + ); + } + } else { + $includeTaxesArr = $xml->getXpath( + "//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes" + ); + $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); + if ($includeTaxesActive) { + $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->TotalChargesWithTaxes->CurrencyCode + ); + } else { + $cost = $shipElement->TotalCharges->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->TotalCharges->CurrencyCode + ); + } + } + + //convert price with Origin country currency code to base currency code + $successConversion = true; + if ($responseCurrencyCode) { + if (in_array($responseCurrencyCode, $allowedCurrencies)) { + $cost = (double)$cost * $this->_getBaseCurrencyRate($responseCurrencyCode); + } else { + $errorTitle = __( + 'We can\'t convert a rate from "%1-%2".', + $responseCurrencyCode, + $this->_request->getPackageCurrency()->getCode() + ); + $error = $this->_rateErrorFactory->create(); + $error->setCarrier('ups'); + $error->setCarrierTitle($this->getConfigData('title')); + $error->setErrorMessage($errorTitle); + $successConversion = false; + } + } + + if ($successConversion) { + $costArr[$code] = $cost; + $priceArr[$code] = $this->getMethodPrice((float)$cost, $code); + } + } + } + /** * Get tracking * @@ -1092,54 +1133,7 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) if ($activityTags) { $index = 1; foreach ($activityTags as $activityTag) { - $addressArr = []; - if (isset($activityTag->ActivityLocation->Address->City)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->City; - } - if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; - } - if (isset($activityTag->ActivityLocation->Address->CountryCode)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; - } - $dateArr = []; - $date = (string)$activityTag->Date; - //YYYYMMDD - $dateArr[] = substr($date, 0, 4); - $dateArr[] = substr($date, 4, 2); - $dateArr[] = substr($date, -2, 2); - - $timeArr = []; - $time = (string)$activityTag->Time; - //HHMMSS - $timeArr[] = substr($time, 0, 2); - $timeArr[] = substr($time, 2, 2); - $timeArr[] = substr($time, -2, 2); - - if ($index === 1) { - $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; - $resultArr['deliverydate'] = implode('-', $dateArr); - //YYYY-MM-DD - $resultArr['deliverytime'] = implode(':', $timeArr); - //HH:MM:SS - $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; - $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; - if ($addressArr) { - $resultArr['deliveryto'] = implode(', ', $addressArr); - } - } else { - $tempArr = []; - $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; - $tempArr['deliverydate'] = implode('-', $dateArr); - //YYYY-MM-DD - $tempArr['deliverytime'] = implode(':', $timeArr); - //HH:MM:SS - if ($addressArr) { - $tempArr['deliverylocation'] = implode(', ', $addressArr); - } - $packageProgress[] = $tempArr; - } - $index++; + $this->processActivityTagInfo($activityTag, $index, $resultArr, $packageProgress); } $resultArr['progressdetail'] = $packageProgress; } @@ -1172,6 +1166,70 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) return $this->_result; } + /** + * Process tracking info from activity tag + * + * @param \Magento\Framework\Simplexml\Element $activityTag + * @param int $index + * @param array $resultArr + * @param array $packageProgress + */ + private function processActivityTagInfo( + \Magento\Framework\Simplexml\Element $activityTag, + int &$index, + array &$resultArr, + array &$packageProgress + ) { + $addressArr = []; + if (isset($activityTag->ActivityLocation->Address->City)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->City; + } + if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; + } + if (isset($activityTag->ActivityLocation->Address->CountryCode)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; + } + $dateArr = []; + $date = (string)$activityTag->Date; + //YYYYMMDD + $dateArr[] = substr($date, 0, 4); + $dateArr[] = substr($date, 4, 2); + $dateArr[] = substr($date, -2, 2); + + $timeArr = []; + $time = (string)$activityTag->Time; + //HHMMSS + $timeArr[] = substr($time, 0, 2); + $timeArr[] = substr($time, 2, 2); + $timeArr[] = substr($time, -2, 2); + + if ($index === 1) { + $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; + $resultArr['deliverydate'] = implode('-', $dateArr); + //YYYY-MM-DD + $resultArr['deliverytime'] = implode(':', $timeArr); + //HH:MM:SS + $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; + $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; + if ($addressArr) { + $resultArr['deliveryto'] = implode(', ', $addressArr); + } + } else { + $tempArr = []; + $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; + $tempArr['deliverydate'] = implode('-', $dateArr); + //YYYY-MM-DD + $tempArr['deliverytime'] = implode(':', $timeArr); + //HH:MM:SS + if ($addressArr) { + $tempArr['deliverylocation'] = implode(', ', $addressArr); + } + $packageProgress[] = $tempArr; + } + $index++; + } + /** * Get tracking response * @@ -1470,6 +1528,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) $shippingLabelContent = (string)$response->ShipmentResults->PackageResults->LabelImage->GraphicImage; $trackingNumber = (string)$response->ShipmentResults->PackageResults->TrackingNumber; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $result->setShippingLabelContent(base64_decode($shippingLabelContent)); $result->setTrackingNumber($trackingNumber); } @@ -1700,6 +1759,7 @@ public function getCustomizableContainerTypes() /** * Get delivery confirmation level based on origin/destination + * * Return null if delivery confirmation is not acceptable * * @param string|null $countyDestination diff --git a/app/code/Magento/Ups/Test/Mftf/Data/ShippingMethodsData.xml b/app/code/Magento/Ups/Test/Mftf/Data/ShippingMethodsData.xml new file mode 100644 index 0000000000000..d4156d4f3358b --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Data/ShippingMethodsData.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="ShippingMethodsUpsTypeSetDefault" type="shipping_methods_ups_type_config"> + <requiredEntity type="ups_type_inherit">ShippingMethodsUpsTypeDefault</requiredEntity> + </entity> + <entity name="ShippingMethodsUpsTypeDefault" type="ups_type_inherit"> + <data key="inherit">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Ups/Test/Mftf/Metadata/shipping_methods_ups_type_config-meta.xml b/app/code/Magento/Ups/Test/Mftf/Metadata/shipping_methods_ups_type_config-meta.xml new file mode 100644 index 0000000000000..d642b7923282e --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Metadata/shipping_methods_ups_type_config-meta.xml @@ -0,0 +1,21 @@ +<?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="ShippingMethodsUpsTypeConfig" dataType="shipping_methods_ups_type_config" type="create" auth="adminFormKey" url="admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="shipping_methods_ups_type_config"> + <object key="ups" dataType="shipping_methods_ups_type_config"> + <object key="fields" dataType="shipping_methods_ups_type_config"> + <object key="type" dataType="ups_type_inherit"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Ups/Test/Mftf/Page/AdminShippingMethodsConfigPage.xml b/app/code/Magento/Ups/Test/Mftf/Page/AdminShippingMethodsConfigPage.xml new file mode 100644 index 0000000000000..ebc44aace6dfb --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Page/AdminShippingMethodsConfigPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminShippingMethodsConfigPage" url="admin/system_config/edit/section/carriers/" area="admin" module="Magento_Ups"> + <section name="AdminShippingMethodsUpsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml new file mode 100644 index 0000000000000..4107f17dbc18c --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodsUpsSection"> + <element name="carriersUpsTab" type="button" selector="#carriers_ups-head"/> + <element name="carriersUpsType" type="select" selector="#carriers_ups_type"/> + <element name="selectedUpsType" type="text" selector="#carriers_ups_type option[selected]"/> + </section> +</sections> diff --git a/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml new file mode 100644 index 0000000000000..51db704a7abc7 --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.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="DefaultConfigForUPSTypeTest"> + <annotations> + <title value="Default Configuration for UPS Type"/> + <description value="Default Configuration for UPS Type"/> + <features value="Ups"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-99012"/> + <useCaseId value="MAGETWO-98947"/> + <group value="ups"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Collapse UPS tab and logout--> + <comment userInput="Collapse UPS tab and logout" stepKey="collapseTabAndLogout"/> + <click selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" stepKey="collapseTab"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Set shipping methods UPS type to default --> + <comment userInput="Set shipping methods UPS type to default" stepKey="setToDefaultShippingMethodsUpsType"/> + <createData entity="ShippingMethodsUpsTypeSetDefault" stepKey="setShippingMethodsUpsTypeToDefault"/> + <!-- Navigate to Stores -> Configuration -> Sales -> Shipping Methods Page --> + <comment userInput="Navigate to Stores -> Configuration -> Sales -> Shipping Methods Page" stepKey="goToAdminShippingMethodsPage"/> + <amOnPage url="{{AdminShippingMethodsConfigPage.url}}" stepKey="navigateToAdminShippingMethodsPage"/> + <waitForPageLoad stepKey="waitPageToLoad"/> + <!-- Expand 'UPS' tab --> + <comment userInput="Expand UPS tab" stepKey="expandUpsTab"/> + <conditionalClick selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" dependentSelector="{{AdminShippingMethodsUpsSection.carriersUpsType}}" visible="false" stepKey="expandTab"/> + <waitForElementVisible selector="{{AdminShippingMethodsUpsSection.carriersUpsType}}" stepKey="waitTabToExpand"/> + <!-- Assert that selected UPS type by default is 'United Parcel Service XML' --> + <comment userInput="Check that selected UPS type by default is 'United Parcel Service XML'" stepKey="assertDefUpsType"/> + <grabTextFrom selector="{{AdminShippingMethodsUpsSection.selectedUpsType}}" stepKey="grabSelectedOptionText"/> + <assertEquals expected='United Parcel Service XML' expectedType="string" actual="($grabSelectedOptionText)" stepKey="assertDefaultUpsType"/> + </test> +</tests> diff --git a/app/code/Magento/Ups/etc/config.xml b/app/code/Magento/Ups/etc/config.xml index e2ac1c6d6c443..73b10dd5ff41b 100644 --- a/app/code/Magento/Ups/etc/config.xml +++ b/app/code/Magento/Ups/etc/config.xml @@ -19,7 +19,7 @@ <cutoff_cost /> <dest_type>RES</dest_type> <free_method>GND</free_method> - <gateway_url>http://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> + <gateway_url>https://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> <gateway_xml_url>https://onlinetools.ups.com/ups.app/xml/Rate</gateway_xml_url> <handling>0</handling> <model>Magento\Ups\Model\Carrier</model> @@ -37,7 +37,7 @@ <negotiated_active>0</negotiated_active> <include_taxes>0</include_taxes> <mode_xml>1</mode_xml> - <type>UPS</type> + <type>UPS_XML</type> <is_account_live>0</is_account_live> <active_rma>0</active_rma> <is_online>1</is_online> diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index d8ceb16d71fdc..2ac1bdd712114 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -105,42 +105,18 @@ protected function doFindOneByData(array $data) $result = null; $requestPath = $data[UrlRewrite::REQUEST_PATH]; - - $data[UrlRewrite::REQUEST_PATH] = [ + $decodedRequestPath = urldecode($requestPath); + $data[UrlRewrite::REQUEST_PATH] = array_unique([ rtrim($requestPath, '/'), rtrim($requestPath, '/') . '/', - ]; + rtrim($decodedRequestPath, '/'), + rtrim($decodedRequestPath, '/') . '/', + ]); $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); - - if (count($resultsFromDb) === 1) { - $resultFromDb = current($resultsFromDb); - $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; - - // If request path matches the DB value or it's redirect - we can return result from DB - $canReturnResultFromDb = ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath - || in_array((int)$resultFromDb[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true)); - - // Otherwise return 301 redirect to request path from DB results - $result = $canReturnResultFromDb ? $resultFromDb : [ - UrlRewrite::ENTITY_TYPE => 'custom', - UrlRewrite::ENTITY_ID => '0', - UrlRewrite::REQUEST_PATH => $requestPath, - UrlRewrite::TARGET_PATH => $resultFromDb[UrlRewrite::REQUEST_PATH], - UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, - UrlRewrite::STORE_ID => $resultFromDb[UrlRewrite::STORE_ID], - UrlRewrite::DESCRIPTION => null, - UrlRewrite::IS_AUTOGENERATED => '0', - UrlRewrite::METADATA => null, - ]; - } else { - // If we have 2 results - return the row that matches request path - foreach ($resultsFromDb as $resultFromDb) { - if ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath) { - $result = $resultFromDb; - break; - } - } + if ($resultsFromDb) { + $urlRewrite = $this->extractMostRelevantUrlRewrite($requestPath, $resultsFromDb); + $result = $this->prepareUrlRewrite($requestPath, $urlRewrite); } return $result; @@ -149,6 +125,75 @@ protected function doFindOneByData(array $data) return $this->connection->fetchRow($this->prepareSelect($data)); } + /** + * Extract most relevant url rewrite from url rewrites list + * + * @param string $requestPath + * @param array $urlRewrites + * @return array|null + */ + private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRewrites): ?array + { + $prioritizedUrlRewrites = []; + foreach ($urlRewrites as $urlRewrite) { + switch (true) { + case $urlRewrite[UrlRewrite::REQUEST_PATH] === $requestPath: + $priority = 1; + break; + case $urlRewrite[UrlRewrite::REQUEST_PATH] === urldecode($requestPath): + $priority = 2; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim($requestPath, '/'): + $priority = 3; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim(urldecode($requestPath), '/'): + $priority = 4; + break; + default: + $priority = 5; + break; + } + $prioritizedUrlRewrites[$priority] = $urlRewrite; + } + ksort($prioritizedUrlRewrites); + + return array_shift($prioritizedUrlRewrites); + } + + /** + * Prepare url rewrite + * + * If request path matches the DB value or it's redirect - we can return result from DB + * Otherwise return 301 redirect to request path from DB results + * + * @param string $requestPath + * @param array $urlRewrite + * @return array + */ + private function prepareUrlRewrite(string $requestPath, array $urlRewrite): array + { + $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; + $canReturnResultFromDb = ( + in_array($urlRewrite[UrlRewrite::REQUEST_PATH], [$requestPath, urldecode($requestPath)], true) + || in_array((int) $urlRewrite[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true) + ); + if (!$canReturnResultFromDb) { + $urlRewrite = [ + UrlRewrite::ENTITY_TYPE => 'custom', + UrlRewrite::ENTITY_ID => '0', + UrlRewrite::REQUEST_PATH => $requestPath, + UrlRewrite::TARGET_PATH => $urlRewrite[UrlRewrite::REQUEST_PATH], + UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, + UrlRewrite::STORE_ID => $urlRewrite[UrlRewrite::STORE_ID], + UrlRewrite::DESCRIPTION => null, + UrlRewrite::IS_AUTOGENERATED => '0', + UrlRewrite::METADATA => null, + ]; + } + + return $urlRewrite; + } + /** * Delete old URLs from DB. * diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index 2ce00d53588b3..e1bb094e7fc39 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -40,6 +40,8 @@ public function __construct( } /** + * Switch to another store. + * * @param StoreInterface $fromStore * @param StoreInterface $targetStore * @param string $redirectUrl @@ -66,17 +68,29 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s UrlRewrite::STORE_ID => $oldStoreId, ]); if ($oldRewrite) { + $targetUrl = $targetStore->getBaseUrl(); // look for url rewrite match on the target store + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), + UrlRewrite::STORE_ID => $targetStore->getId(), + ]); + if ($currentRewrite) { + $targetUrl .= $currentRewrite->getRequestPath(); + } + } else { + $existingRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::REQUEST_PATH => $urlPath + ]); $currentRewrite = $this->urlFinder->findOneByData([ UrlRewrite::REQUEST_PATH => $urlPath, UrlRewrite::STORE_ID => $targetStore->getId(), ]); - if (null === $currentRewrite) { + + if ($existingRewrite && !$currentRewrite) { /** @var \Magento\Framework\App\Response\Http $response */ $targetUrl = $targetStore->getBaseUrl(); } } - return $targetUrl; } } diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml new file mode 100644 index 0000000000000..50b83641e19a9 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml @@ -0,0 +1,111 @@ +<?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="AdminAddUrlRewrite"> + <arguments> + <argument name="category" type="string"/> + <argument name="customUrlRewriteValue" type="string"/> + <argument name="storeValue" type="string"/> + <argument name="requestPath" type="string"/> + <argument name="redirectTypeValue" type="string"/> + <argument name="description" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteEditPage.url}}" stepKey="openUrlRewriteEditPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad"/> + <click selector="{{AdminUrlRewriteEditSection.createCustomUrlRewrite}}" stepKey="clickOnCustonUrlRewrite"/> + <click selector="{{AdminUrlRewriteEditSection.createCustomUrlRewriteValue('customUrlRewriteValue')}}" stepKey="selectForCategory"/> + <waitForPageLoad stepKey="waitForCategoryEditSectionToLoad"/> + <click selector="{{AdminUrlRewriteEditSection.categoryInTree($$category.name$$)}}" stepKey="selectCategoryInTree"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> + <click selector="{{AdminUrlRewriteEditSection.storeValue('storeValue')}}" stepKey="clickOnStoreValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> + <click selector="{{AdminUrlRewriteEditSection.redirectType}}" stepKey="selectRedirectType"/> + <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue('redirectTypeValue')}}" stepKey="clickOnRedirectTypeValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> + <actionGroup name="AdminAddUrlRewriteForProduct"> + <arguments> + <argument name="storeValue" type="string"/> + <argument name="requestPath" type="string"/> + <argument name="redirectTypeValue" type="string"/> + <argument name="description" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminUrlRewriteProductSection.skipCategoryButton}}" stepKey="waitForSkipCategoryButton"/> + <click selector="{{AdminUrlRewriteProductSection.skipCategoryButton}}" stepKey="clickOnSkipCategoryButton"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> + <click selector="{{AdminUrlRewriteEditSection.storeValue('storeValue')}}" stepKey="clickOnStoreValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> + <click selector="{{AdminUrlRewriteEditSection.redirectType}}" stepKey="selectRedirectType"/> + <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue('redirectTypeValue')}}" stepKey="clickOnRedirectTypeValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> + <actionGroup name="AdminAddCustomUrlRewrite"> + <arguments> + <argument name="customUrlRewriteValue" type="string"/> + <argument name="storeValue" type="string"/> + <argument name="requestPath" type="string"/> + <argument name="targetPath" type="string"/> + <argument name="redirectTypeValue" type="string"/> + <argument name="description" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteEditPage.url}}" stepKey="openUrlRewriteEditPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad" after="openUrlRewriteEditPage"/> + <click selector="{{AdminUrlRewriteEditSection.createCustomUrlRewrite}}" stepKey="clickOnCustonUrlRewrite"/> + <click selector="{{AdminUrlRewriteEditSection.createCustomUrlRewriteValue('customUrlRewriteValue')}}" stepKey="selectCustom"/> + <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> + <click selector="{{AdminUrlRewriteEditSection.storeValue('storeValue')}}" stepKey="clickOnStoreValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> + <fillField selector="{{AdminUrlRewriteEditSection.targetPath}}" userInput="{{targetPath}}" stepKey="fillTargetPath"/> + <click selector="{{AdminUrlRewriteEditSection.redirectType}}" stepKey="selectRedirectType"/> + <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue('redirectTypeValue')}}" stepKey="selectRedirectTypeValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> + <actionGroup name="AdminUpdateUrlRewrite"> + <arguments> + <argument name="storeValue" type="string"/> + <argument name="requestPath" type="string"/> + <argument name="redirectTypeValue" type="string"/> + <argument name="description" type="string"/> + </arguments> + <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> + <click selector="{{AdminUrlRewriteEditSection.storeValue(storeValue)}}" stepKey="clickOnStoreValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> + <click selector="{{AdminUrlRewriteEditSection.redirectType}}" stepKey="selectRedirectType"/> + <click selector="{{AdminUrlRewriteEditSection.redirectTypeValue(redirectTypeValue)}}" stepKey="selectRedirectTypeValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> + <actionGroup name="AdminUpdateCustomUrlRewrite"> + <arguments> + <argument name="storeValue" type="string"/> + <argument name="requestPath" type="string"/> + <argument name="targetPath" type="string"/> + <argument name="redirectTypeValue" type="string"/> + <argument name="description" type="string"/> + </arguments> + <click selector="{{AdminUrlRewriteEditSection.store}}" stepKey="clickOnStore"/> + <click selector="{{AdminUrlRewriteEditSection.storeValue('storeValue')}}" stepKey="clickOnStoreValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.requestPath}}" userInput="{{requestPath}}" stepKey="fillRequestPath"/> + <fillField selector="{{AdminUrlRewriteEditSection.targetPath}}" userInput="{{targetPath}}" stepKey="fillTargetPath"/> + <selectOption selector="{{AdminUrlRewriteEditSection.redirectType}}" userInput="{{redirectTypeValue}}" stepKey="selectRedirectTypeValue"/> + <fillField selector="{{AdminUrlRewriteEditSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <click selector="{{AdminUrlRewriteEditSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.successMessage}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteGridActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteGridActionGroup.xml new file mode 100644 index 0000000000000..1a9248ef36789 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteGridActionGroup.xml @@ -0,0 +1,97 @@ +<?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="AdminSearchByRequestPath"> + <arguments> + <argument name="redirectPath" type="string"/> + <argument name="redirectType" type="string"/> + <argument name="targetPath" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteEditPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad"/> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{redirectPath}}" stepKey="fillRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{redirectPath}}" stepKey="seeTheRedirectPathForOldUrl"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{targetPath}}" stepKey="seeTheTargetPath" /> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="{{redirectType}}" stepKey="seeTheRedirectTypeForOldUrl" /> + </actionGroup> + <actionGroup name="AdminSearchUrlRewriteProductBySku"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteProductPage.url}}" stepKey="openUrlRewriteProductPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteProductPageToLoad"/> + <click selector="{{AdminUrlRewriteProductSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminUrlRewriteProductSection.skuFilter}}" userInput="{{productSku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminUrlRewriteProductSection.searchFilter}}" stepKey="clickOnSearchFilter"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <click selector="{{AdminUrlRewriteProductSection.productRow}}" stepKey="clickOnFirstRow"/> + <waitForPageLoad stepKey="waitForProductCategoryPageToLoad"/> + </actionGroup> + <actionGroup name="AdminSearchDeletedUrlRewrite"> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteEditPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad"/> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{requestPath}}" stepKey="fillRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <see selector="{{AdminUrlRewriteIndexSection.emptyRecords}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> + </actionGroup> + <actionGroup name="AdminDeleteUrlRewrite"> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteEditPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad"/> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{requestPath}}" stepKey="fillRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminUrlRewriteIndexSection.editButton('1')}}" stepKey="clickOnEditButton"/> + <waitForPageLoad stepKey="waitForEditPageToLoad"/> + <click selector="{{AdminUrlRewriteEditSection.deleteButton}}" stepKey="clickOnDeleteButton"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <waitForElementVisible selector="{{AdminUrlRewriteEditSection.okButton}}" stepKey="waitForOkButtonToVisible"/> + <click selector="{{AdminUrlRewriteEditSection.okButton}}" stepKey="clickOnOkButton"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminUrlRewriteIndexSection.successMessage}}" userInput="You deleted the URL rewrite." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertPageByUrlRewriteIsNotFound"> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + <amOnPage url="{{requestPath}}" stepKey="amOnPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <see userInput="Whoops, our bad..." stepKey="seeWhoops"/> + </actionGroup> + <actionGroup name="AdminSearchAndSelectUrlRewriteInGrid"> + <arguments> + <argument name="requestPath" type="string"/> + </arguments> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteEditPage"/> + <waitForPageLoad stepKey="waitForUrlRewriteEditPageToLoad"/> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{requestPath}}" stepKey="fillRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminUrlRewriteIndexSection.editButton('1')}}" stepKey="clickOnEditButton"/> + <waitForPageLoad stepKey="waitForEditPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontUrlRewriteRedirectActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontUrlRewriteRedirectActionGroup.xml new file mode 100644 index 0000000000000..a299e1689d6a7 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontUrlRewriteRedirectActionGroup.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="AssertStorefrontUrlRewriteRedirect"> + <arguments> + <argument name="category" type="string"/> + <argument name="newRequestPath" type="string"/> + </arguments> + <amOnPage url="{{newRequestPath}}" stepKey="openCategoryInStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(category)}}" stepKey="seeCategoryOnStoreNavigationBar"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(category)}}" stepKey="seeCategoryInTitle"/> + </actionGroup> + <actionGroup name="AssertStorefrontProductRedirect"> + <arguments> + <argument name="productName" type="string"/> + <argument name="productSku" type="string"/> + <argument name="productRequestPath" type="string"/> + </arguments> + <amOnPage url="{{productRequestPath}}" stepKey="openCategoryInStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}" stepKey="seeProductNameInStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{productSku}}" stepKey="seeProductSkuInStoreFrontPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..66eb3c9ba9f46 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/AdminMenuData.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="AdminMenuMarketingSEOAndSearchURLRewrites"> + <data key="pageTitle">URL Rewrites</data> + <data key="title">URL Rewrites</data> + <data key="dataUiId">magento-urlrewrite-urlrewrite</data> + </entity> +</entities> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml new file mode 100644 index 0000000000000..3692e82072afc --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml @@ -0,0 +1,47 @@ +<?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="defaultUrlRewrite" type="urlRewrite"> + <data key="request_path" unique="prefix">test-test-test.html</data> + <data key="target_path">http://www.example.com/</data> + <data key="redirect_type">302</data> + <data key="redirect_type_label">Temporary (302)</data> + <data key="store_id">1</data> + <data key="store">Default Store View</data> + <data key="description">End To End Test</data> + </entity> + <entity name="updateUrlRewrite" type="urlRewrite"> + <data key="request_path" unique="prefix">test-aspx-test.aspx</data> + <data key="target_path">http://www.example.com/</data> + <data key="redirect_type">302</data> + <data key="redirect_type_label">Temporary (302)</data> + <data key="store_id">1</data> + <data key="store">Default Store View</data> + <data key="description">Update Url Rewrite</data> + </entity> + <entity name="customPermanentUrlRewrite" type="urlRewrite"> + <data key="request_path" unique="prefix">wishlist</data> + <data key="target_path">https://marketplace.magento.com/</data> + <data key="redirect_type">301</data> + <data key="redirect_type_label">Permanent (301)</data> + <data key="store_id">1</data> + <data key="store">Default Store View</data> + <data key="description">test_description_relative path</data> + </entity> + <entity name="customTemporaryUrlRewrite" type="urlRewrite"> + <data key="request_path" unique="prefix">wishlist</data> + <data key="target_path">https://marketplace.magento.com/</data> + <data key="redirect_type">302</data> + <data key="redirect_type_label">Temporary (302)</data> + <data key="store_id">1</data> + <data key="store">Default Store View</data> + <data key="description">test_description_relative path</data> + </entity> +</entities> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Metadata/url_rewrite-meta.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Metadata/url_rewrite-meta.xml new file mode 100644 index 0000000000000..0738b17d6e0f0 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Metadata/url_rewrite-meta.xml @@ -0,0 +1,18 @@ +<?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="CreateUrlRewrite" dataType="urlRewrite" type="create" auth="adminFormKey" url="/admin/url_rewrite/save/" method="POST" successRegex="/messages-message-success/"> + <field key="store_id">integer</field> + <field key="redirect_type">integer</field> + <field key="request_path">string</field> + <field key="target_path">string</field> + <field key="description">string</field> + </operation> +</operations> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml new file mode 100644 index 0000000000000..b43e0e05ad55d --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminUrlRewriteEditPage" url="admin/url_rewrite/edit/id/{{url_rewrite_id}}/" area="admin" module="Magento_UrlRewrite"> + <section name="AdminUrlRewriteEditSection"/> + </page> +</pages> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductPage.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductPage.xml new file mode 100644 index 0000000000000..645396bc778e9 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminUrlRewriteProductPage" url="admin/url_rewrite/edit/product" area="admin" module="Magento_UrlRewrite"> + <section name="AdminUrlRewriteProductSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml new file mode 100644 index 0000000000000..52939607f5377 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminUrlRewriteEditSection"> + <element name="createCustomUrlRewrite" type="select" selector="//select[@id='entity-type-selector']" /> + <element name="createCustomUrlRewriteValue" type="text" selector="//select[@id='entity-type-selector']/option[contains(.,'{{var}}')]" parameterized="true"/> + <element name="store" type="select" selector="//select[@id='store_id']"/> + <element name="storeValue" type="select" selector="//select[@id='store_id']//option[contains(., '{{var}}')]" parameterized="true" /> + <element name="requestPath" type="input" selector="//input[@id='request_path']"/> + <element name="targetPath" type="input" selector="//input[@id='target_path']"/> + <element name="redirectType" type="select" selector="//select[@id='redirect_type']"/> + <element name="redirectTypeValue" type="select" selector="//select[@id='redirect_type']//option[contains(., '{{Var}}')]" parameterized="true"/> + <element name="description" type="input" selector="#description"/> + <element name="categoryInTree" type="text" selector="//li[contains(@class,'active-category jstree-open')]/a[contains(., '{{categoryName}}')]" parameterized="true"/> + <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="deleteButton" type="button" selector="#delete" timeout="30"/> + <element name="okButton" type="button" selector="//button[@class='action-primary action-accept']" timeout="30"/> + <element name="requestPathField" type="input" selector="#request_path"/> + </section> +</sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml index 0880b50950e15..7b789845fe249 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -11,5 +11,16 @@ <section name="AdminUrlRewriteIndexSection"> <element name="requestPathFilter" type="input" selector="#urlrewriteGrid_filter_request_path"/> <element name="requestPathColumnValue" type="text" selector="//*[@id='urlrewriteGrid']//tbody//td[@data-column='request_path' and normalize-space(.)='{{columnValue}}']" parameterized="true"/> + <element name="targetPathColumnValue" type="text" selector="//*[@id='urlrewriteGrid']//tbody//td[@data-column='target_path' and normalize-space(.)='{{columnValue}}']" parameterized="true"/> + <element name="searchButton" type="button" selector="//button[@data-ui-id='widget-button-1']" timeout="30"/> + <element name="resetButton" type="button" selector="button[data-ui-id='widget-button-0']" timeout="30"/> + <element name="emptyRecordMessage" type="text" selector="//*[@class='empty-text']"/> + <element name="targetPathColumn" type="text" selector="//tr[@data-role='row'][{{var1}}]/td[@data-column='target_path']" parameterized="true"/> + <element name="redirectTypeColumn" type="text" selector="//tr[@data-role='row'][{{var1}}]/td[@data-column='redirect_type']" parameterized="true"/> + <element name="requestPathColumn" type="text" selector="//tr[@data-role='row'][{{var1}}]/td[@data-column='request_path']" parameterized="true"/> + <element name="emptyRecords" type="text" selector="//td[@class='empty-text']"/> + <element name="successMessage" type="text" selector="#messages"/> + <element name="editButton" type="text" selector="//tr[@data-role='row'][{{rowNumber}}]/td/a[contains(.,'Edit')]" parameterized="true"/> + <element name="storeView" type="text" selector="//tr[@data-role='row'][{{var1}}]/td[@data-column='store_id']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteProductSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteProductSection.xml new file mode 100644 index 0000000000000..3650ec56d1391 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteProductSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminUrlRewriteProductSection"> + <element name="skuFilter" type="input" selector="//input[@name='sku']"/> + <element name="resetFilter" type="button" selector="//button[@data-action='grid-filter-reset']" timeout="30"/> + <element name="searchFilter" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> + <element name="productRow" type="text" selector="//tbody/tr/td[contains(@class,'col-sku')]"/> + <element name="skipCategoryButton" type="button" selector="//button[@class='action-default scalable save']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml new file mode 100644 index 0000000000000..52d313b21f3e1 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml @@ -0,0 +1,73 @@ +<?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="AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest"> + <annotations> + <stories value="Create Product UrlRewrite"/> + <title value="Create product URL rewrite, autoupdate if subcategory deleted"/> + <description value="Login as admin,verify UrlRewrite auto update when subcategory is deleted "/> + <testCaseId value="MC-5342"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter and Select the created Product --> + <actionGroup ref="AdminSearchUrlRewriteProductBySku" stepKey="searchProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Update the Store, RequestPath, RedirectType and Description --> + <actionGroup ref="AdminAddUrlRewriteForProduct" stepKey="addUrlRewriteForProduct"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="$$createSimpleProduct.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="{{_defaultProduct.urlKey}}.html" /> + <argument name="redirectType" value="Temporary (302)"/> + <argument name="targetPath" value="$$createSimpleProduct.name$$.html"/> + </actionGroup> + + <!--Assert Category Url Redirect is not present --> + <actionGroup ref="AdminSearchDeletedUrlRewrite" stepKey="searchDeletedCategory"> + <argument name="requestPath" value="$$createCategory.name$$.html"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml new file mode 100644 index 0000000000000..2c2dd48caeaa9 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml @@ -0,0 +1,117 @@ +<?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="AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Url Rewrites for Multiple Storeviews"/> + <title value="Url Rewrites Correctly Generated for Multiple Storeviews During Product Import"/> + <description value="Check Url Rewrites Correctly Generated for Multiple Storeviews During Product Import."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-68980"/> + <group value="urlRewrite"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create Store View EN --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> + <argument name="customStore" value="customStoreENNotUnique"/> + </actionGroup> + <!-- Create Store View NL --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewNl"> + <argument name="customStore" value="customStoreNLNotUnique"/> + </actionGroup> + <createData entity="ApiCategory" stepKey="createCategory"> + <field key="name">category-admin</field> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductByName" stepKey="deleteImportedProduct"> + <argument name="sku" value="productformagetwo68980"/> + <argument name="name" value="productformagetwo68980"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFiltersIfSet"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreENNotUnique"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewNl"> + <argument name="customStore" value="customStoreNLNotUnique"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewEn"> + <argument name="Store" value="customStoreENNotUnique.name"/> + <argument name="CatName" value="$$createCategory.name$$"/> + </actionGroup> + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyENStoreView"> + <argument name="value" value="category-english"/> + </actionGroup> + <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewNl"> + <argument name="Store" value="customStoreNLNotUnique.name"/> + <argument name="CatName" value="$$createCategory.name$$"/> + </actionGroup> + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyNLStoreView"> + <argument name="value" value="category-dutch"/> + </actionGroup> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> + <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> + <argument name="productName" value="productformagetwo68980"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue('catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue('catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-english/productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-dutch/productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView3"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml new file mode 100644 index 0000000000000..52dce4d67f698 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml @@ -0,0 +1,82 @@ +<?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="AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Update url rewrites"/> + <title value="Check url rewrites in catalog categories after changing url key"/> + <description value="Check url rewrites in catalog categories after changing url key for store view and moving category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5352"/> + <group value="url_rewrite"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create two sub-categories in default category with simple products --> + <createData entity="_defaultCategory" stepKey="createFirstCategory"/> + <createData entity="_defaultProduct" stepKey="createFirstSimpleProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="_defaultCategory" stepKey="createSecondCategory"/> + <createData entity="_defaultProduct" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + + <!-- Log in to backend --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create additional Store View in Main Website Store --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> + </before> + <after> + <deleteData createDataKey="createFirstCategory" stepKey="deleteFirstCategory"/> + <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- On the categories editing page change store view to created additional view --> + <actionGroup ref="switchCategoryStoreView" stepKey="switchStoreView"> + <argument name="Store" value="customStore.name"/> + <argument name="CatName" value="$$createFirstCategory.name$$"/> + </actionGroup> + + <!-- Change url key for category for first category; save --> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeUrlKey"> + <argument name="value" value="{{SimpleRootSubCategory.url_key}}"/> + </actionGroup> + + <!-- Change store view to "All store views" for first category --> + <actionGroup ref="switchCategoryToAllStoreView" stepKey="switchToAllStoreViewProduct"> + <argument name="CatName" value="$$createFirstCategory.name$$"/> + </actionGroup> + + <!-- Move first category inside second category --> + <actionGroup ref="MoveCategoryActionGroup" stepKey="moveFirstCategoryToSecondCategory"> + <argument name="childCategory" value="$$createFirstCategory.name$$"/> + <argument name="parentCategory" value="$$createSecondCategory.name$$"/> + </actionGroup> + + <!-- Switch default store view on store view created below for first category --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <!-- Assert category url with custom store view --> + <amOnPage url="{{StorefrontHomePage.url}}$$createSecondCategory.name$$/{{SimpleRootSubCategory.url_key}}.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee userInput="$$createSecondSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml new file mode 100644 index 0000000000000..a7a7c0c73d826 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryUrlRewriteAndAddNoRedirectTest"> + <annotations> + <stories value="Create category URL rewrite"/> + <title value="Create category URL rewrite, with no redirect"/> + <description value="Login as admin and create category UrlRewrite with No redirect"/> + <testCaseId value="MC-5335"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Url Rewrite Index Page and update the Custom Url Rewrite, Store, Request Path, Redirect Type and Description --> + <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> + <argument name="category" value="$$category.name$$"/> + <argument name="customUrlRewriteValue" value="For Category'"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="newrequestpath.html"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!-- Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="newrequestpath.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml new file mode 100644 index 0000000000000..974550bb92214 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryUrlRewriteAndAddPermanentRedirectTest"> + <annotations> + <stories value="Create category URL rewrite"/> + <title value="Create category URL rewrite, add permanent redirect for category"/> + <description value="Login as admin and create category UrlRewrite with Permanent redirect"/> + <testCaseId value="MC-5334"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Url Rewrite Index Page and update the Custom Url Rewrite, Store, Request Path, Redirect Type and Description --> + <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> + <argument name="category" value="$$category.name$$"/> + <argument name="customUrlRewriteValue" value="For Category'"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="newrequestpath.html"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="newrequestpath.html" /> + <argument name="redirectType" value="Permanent (301)" /> + <argument name="targetPath" value="$$category.name_lwr$$.html"/> + </actionGroup> + + <!--Assert Updated path directs to the category storefront --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="openStorefrontUrlRedirectPath"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="newrequestpath.html"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..c64019ea38acc --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Create category URL rewrite"/> + <title value="Create category URL rewrite, with temporary redirect"/> + <description value="Login as admin and create category UrlRewrite with Temporary redirect"/> + <testCaseId value="MC-5336"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Url Rewrite Index Page and update the Custom Url Rewrite, Store, Request Path, Redirect Type and Description --> + <actionGroup ref="AdminAddUrlRewrite" stepKey="addUrlRewrite"> + <argument name="category" value="$$category.name$$"/> + <argument name="customUrlRewriteValue" value="For Category'"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="newrequestpath.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="newrequestpath.html" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="$$category.name_lwr$$.html"/> + </actionGroup> + + <!--Assert Updated path directs to the category storefront --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="openStorefrontUrlRedirectPath"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="newrequestpath.html"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml new file mode 100644 index 0000000000000..358aa58aba0f7 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest.xml @@ -0,0 +1,86 @@ +<?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="AdminCreateCustomCMSPageUrlRewriteAndAddPermanentRedirectTest"> + <annotations> + <stories value="Create custom URL rewrite"/> + <title value="Create custom URL rewrite, CMS permanent"/> + <description value="Login as Admin and create custom CMS page UrlRewrite and add Permanent redirect type "/> + <testCaseId value="MC-5345"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="simpleCmsPage" stepKey="createCMSPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCMSPage" stepKey="deleteCMSPage"/> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{defaultCmsPage.title}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open CMS Edit Page and Get the CMS ID --> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="cmsId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open UrlRewrite Edit page and update the fields and fill the created CMS Page Target Path --> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{defaultCmsPage.title}}"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + <argument name="description" value="Created New CMS Page."/> + </actionGroup> + + <!-- Assert updated CMS page Url Rewrite in Grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{defaultCmsPage.title}}" /> + <argument name="redirectType" value="Permanent (301)" /> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + </actionGroup> + + <!-- Assert initial CMS page Url Rewrite in Grid--> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createCMSPage.identifier$$" /> + <argument name="redirectType" value="No"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + </actionGroup> + + <!-- Assert Updated Request Path redirects to the CMS Page on Store Front --> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront"> + <argument name="page" value="{{defaultCmsPage.title}}"/> + </actionGroup> + + <!-- Assert updated CMS redirect in Store Front --> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + + <!-- Assert initial request path directs to the CMS Page on Store Front --> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront1"> + <argument name="page" value="$$createCMSPage.identifier$$"/> + </actionGroup> + + <!-- Assert initial CMS redirect in Store Front --> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage1"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..e6ee9b484059d --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,86 @@ +<?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="AdminCreateCustomCMSPageUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Create custom URL rewrite"/> + <title value="Create custom URL rewrite, CMS temporary"/> + <description value="Login as Admin and create custom CMS page UrlRewrite and add Temporary redirect type "/> + <testCaseId value="MC-5346"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="simpleCmsPage" stepKey="createCMSPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCMSPage" stepKey="deleteCMSPage"/> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{defaultCmsPage.title}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open CMS Edit Page and Get the CMS ID --> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="cmsId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open UrlRewrite Edit page and update the fields and fill the created CMS Page Target Path --> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{defaultCmsPage.title}}"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + <argument name="description" value="Created New CMS Page."/> + </actionGroup> + + <!-- Assert updated CMS page Url Rewrite in Grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{defaultCmsPage.title}}" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + </actionGroup> + + <!-- Assert initial CMS page Url Rewrite in Grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createCMSPage.identifier$$" /> + <argument name="redirectType" value="No"/> + <argument name="targetPath" value="cms/page/view/page_id/{$cmsId}"/> + </actionGroup> + + <!-- Assert Updated Request Path redirects to the CMS Page on Store Front --> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront"> + <argument name="page" value="{{defaultCmsPage.title}}"/> + </actionGroup> + + <!--Assert updated CMS redirect in Store Front--> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + + <!-- Assert initial request path directs to the CMS Page on Store Front --> + <actionGroup ref="navigateToStorefrontForCreatedPage" stepKey="navigateToTheStoreFront1"> + <argument name="page" value="$$createCMSPage.identifier$$"/> + </actionGroup> + + <!--Assert initial CMS redirect in Store Front--> + <actionGroup ref="AssertStoreFrontCMSPage" stepKey="assertCMSPage1"> + <argument name="cmsTitle" value="$$createCMSPage.title$$"/> + <argument name="cmsContent" value="$$createCMSPage.content$$"/> + <argument name="cmsContentHeading" value="$$createCMSPage.content_heading$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml new file mode 100644 index 0000000000000..b123bc14cb1ed --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -0,0 +1,74 @@ +<?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="AdminCreateCustomCategoryUrlRewriteAndAddPermanentRedirectTest"> + <annotations> + <stories value="Create custom URL rewrite"/> + <title value="Create custom URL rewrite, permanent"/> + <description value="Login as Admin and create custom UrlRewrite and add redirect type permenent"/> + <testCaseId value="MC-5343"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{FirstLevelSubCat.name}}.html"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open UrlRewrite Edit page and update the fields and fill the created category Target Path --> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{FirstLevelSubCat.name}}.html"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!-- Assert updated category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByCategoryRequestPath"> + <argument name="redirectPath" value="$$category.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!--Assert initial category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByNewRequestPath"> + <argument name="redirectPath" value="{{FirstLevelSubCat.name}}.html" /> + <argument name="redirectType" value="Permanent (301)" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!-- Assert updated Category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="{{FirstLevelSubCat.name}}.html"/> + </actionGroup> + + <!-- Assert initial Category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront1"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..711d5389b013b --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,76 @@ +<?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="AdminCreateCustomProductUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Create custom URL rewrite"/> + <title value="Create custom URL rewrite, temporary"/> + <description value="Login as Admin and create custom product UrlRewrite and add Temporary redirect type "/> + <testCaseId value="MC-5344"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{_defaultProduct.name}}.html"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open UrlRewrite Edit page and update the fields and fill the created product Target Path --> + <actionGroup ref="AdminAddCustomUrlRewrite" stepKey="addCustomUrlRewrite"> + <argument name="customUrlRewriteValue" value="Custom"/> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{_defaultProduct.name}}.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Assert updated product Url Rewrite in Grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{_defaultProduct.name}}.html" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- Assert initial product Url rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createProduct.name$$.html" /> + <argument name="redirectType" value="No"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- Assert updated product redirect in Store Front--> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="verifyProductInStoreFront"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="{{_defaultProduct.name}}.html"/> + </actionGroup> + + <!-- Assert initial product redirect in Store Front--> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="verifyProductInStoreFront1"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="$$createProduct.name$$.html"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteAndAddNoRedirectTest.xml new file mode 100644 index 0000000000000..f8d297c92a176 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteAndAddNoRedirectTest.xml @@ -0,0 +1,62 @@ +<?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="AdminCreateProductUrLRewriteAndAddNoRedirectTest"> + <annotations> + <stories value="Create Product UrlRewrite"/> + <title value="Create product URL rewrite, with no redirect"/> + <description value="Login as admin, create product UrlRewrite and add No redirect "/> + <testCaseId value="MC-5339"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter and Select the created Product --> + <actionGroup ref="AdminSearchUrlRewriteProductBySku" stepKey="searchProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Update the Store, RequestPath, RedirectType and Description --> + <actionGroup ref="AdminAddUrlRewriteForProduct" stepKey="addUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!--Assert Product Redirect --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{_defaultProduct.urlKey}}.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createSimpleProduct.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..ae18ab33ba6ce --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest.xml @@ -0,0 +1,79 @@ +<?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="AdminCreateProductURLRewriteWithCategoryAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Create Product UrlRewrite"/> + <title value="Create product URL rewrite, add temporary redirect for product"/> + <description value="Login as admin, create product with category and UrlRewrite and add temporary redirect "/> + <testCaseId value="MC-5338"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter and Select the created Product --> + <actionGroup ref="AdminSearchUrlRewriteProductBySku" stepKey="searchProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Update the Store, RequestPath, RedirectType and Description --> + <actionGroup ref="AdminAddUrlRewriteForProduct" stepKey="addUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{FirstLevelSubCat.name_lwr}}/{{_defaultProduct.urlKey}}.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Assert Product Redirect --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{FirstLevelSubCat.name_lwr}}/{{_defaultProduct.urlKey}}.html" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="$$createSimpleProduct.name$$.html"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createSimpleProduct.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$createCategory.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath2"> + <argument name="redirectPath" value="$$createCategory.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml new file mode 100644 index 0000000000000..66c586d4fe891 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddPermanentRedirectTest.xml @@ -0,0 +1,62 @@ +<?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="AdminCreateProductUrLRewriteAndAddPermanentRedirectTest"> + <annotations> + <stories value="Create Product UrlRewrite"/> + <title value="Create product URL rewrite, with permanent redirect"/> + <description value="Login as admin, create product UrlRewrite and add Permanent redirect"/> + <testCaseId value="MC-5341"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter and Select the created Product --> + <actionGroup ref="AdminSearchUrlRewriteProductBySku" stepKey="searchProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Update the Store, RequestPath, RedirectType and Description --> + <actionGroup ref="AdminAddUrlRewriteForProduct" stepKey="addUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + + <!--Assert Product Redirect --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{_defaultProduct.urlKey}}.html" /> + <argument name="redirectType" value="Permanent (301)" /> + <argument name="targetPath" value="$$createSimpleProduct.name$$.html"/> + </actionGroup> + + <!-- Assert Redirect Path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createSimpleProduct.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..2d797a12bedf5 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,62 @@ +<?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="AdminCreateProductUrLRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Create Product UrlRewrite"/> + <title value="Create product URL rewrite, with temporary redirect"/> + <description value="Login as admin, create product UrlRewrite and add Temporary redirect"/> + <testCaseId value="MC-5340"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter and Select the created Product --> + <actionGroup ref="AdminSearchUrlRewriteProductBySku" stepKey="searchProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Update the Store, RequestPath, RedirectType and Description --> + <actionGroup ref="AdminAddUrlRewriteForProduct" stepKey="addUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!--Assert Product Redirect --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="{{_defaultProduct.urlKey}}.html" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="$$createSimpleProduct.name$$.html"/> + </actionGroup> + + <!-- Assert Redirect Path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="$$createSimpleProduct.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml new file mode 100644 index 0000000000000..83c1e5c0a5e0a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -0,0 +1,113 @@ +<?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="AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest"> + <annotations> + <stories value="Create product with several websites"/> + <title value="Create product with several websites and check URL Rewrites"/> + <description value="Test log in to Create product and Create product with several websites and check URL Rewrites"/> + <testCaseId value="MC-5359"/> + <severity value="CRITICAL"/> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStore1"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStore2"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create first store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStore.name}}"/> + <argument name="storeGroupCode" value="{{customStore.code}}"/> + </actionGroup> + <!-- Create first store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + + <!-- Create second store --> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + + <!-- Create simple product with categories created in create data --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowOfCreatedSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$rootCategory.name$$" stepKey="fillSearchForInitialCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$rootCategory.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$category.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$category.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSaved"/> + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Grab category Id --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="grabCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + <!-- Open Url Rewrite page and verify new Redirect Path, RedirectType and Target Path for the grabbed category Id --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchPath"> + <argument name="redirectPath" value="$$category.name$$.html"/> + <argument name="redirectType" value="No"/> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + <see selector="{{AdminUrlRewriteIndexSection.storeView('1')}}" userInput="{{customStoreGroup.name}}" stepKey="seeStoreValueForCategoryId"/> + <see selector="{{AdminUrlRewriteIndexSection.storeView('1')}}" userInput="{{customStoreEN.name}}" stepKey="seeStoreViewValueForCategoryId"/> + + <!-- Grab product Id --> + <actionGroup ref="filterAndSelectProduct" stepKey="grabProductId"> + <argument name="productSku" value="$$createProduct.sku$$"/> + </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="AdminSearchByRequestPath" stepKey="searchPath1"> + <argument name="redirectPath" value="$$createProduct.name$$.html"/> + <argument name="redirectType" value="No"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + <see selector="{{AdminUrlRewriteIndexSection.storeView('1')}}" userInput="{{customStore.name}}" stepKey="seeStoreValueForProductId"/> + <see selector="{{AdminUrlRewriteIndexSection.storeView('1')}}" userInput="{{storeViewData.name}}" stepKey="seeStoreViewValueForProductId"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml new file mode 100644 index 0000000000000..cf45931029778 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml @@ -0,0 +1,44 @@ +<?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="AdminDeleteCustomUrlRewriteTest"> + <annotations> + <stories value="Delete custom URL rewrite"/> + <title value="Delete custom URL rewrite"/> + <description value="Test log in to URL rewrite and Delete custom URL rewrite"/> + <testCaseId value="MC-5350"/> + <severity value="CRITICAL"/> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultUrlRewrite" stepKey="urlRewrite" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete created custom url rewrite and verify AssertUrlRewriteDeletedMessage--> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteUrlRewrite"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Search and verify AssertUrlRewriteNotInGrid--> + <actionGroup ref="AdminSearchDeletedUrlRewrite" stepKey="searchDeletedUrlRewriteInGrid"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Verify AssertPageByUrlRewriteIsNotFound--> + <actionGroup ref="AssertPageByUrlRewriteIsNotFound" stepKey="amOnPage"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml new file mode 100644 index 0000000000000..443307b427b42 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminMarketingUrlRewritesNavigateMenuTest"> + <annotations> + <features value="UrlRewrite"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing url rewrites navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > URL Rewrites"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14202"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingURLRewritesPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingSEOAndSearchURLRewrites.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingSEOAndSearchURLRewrites.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml new file mode 100644 index 0000000000000..072753505223d --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml @@ -0,0 +1,61 @@ +<?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="AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest"> + <annotations> + <stories value="Update URL rewrite"/> + <title value="Update Category URL Rewrites, aspx request path"/> + <description value="Login as Admin, update category UrlRewrite, add aspx request path and Temporary redirect type "/> + <testCaseId value="MC-5358"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created category in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$category.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{defaultUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="Update Category Url Rewrite"/> + </actionGroup> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!--Assert category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByNewRequestPath"> + <argument name="redirectPath" value="{{defaultUrlRewrite.request_path}}" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!-- Assert category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="{{defaultUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml new file mode 100644 index 0000000000000..80b9dbe41bf59 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest.xml @@ -0,0 +1,61 @@ +<?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="AdminUpdateCategoryUrlRewriteAndAddNoRedirectTest"> + <annotations> + <stories value="Update URL rewrite"/> + <title value="Update Category URL Rewrites, no redirect type"/> + <description value="Login as Admin and update category Url Rewrite and add redirect type No"/> + <testCaseId value="MC-5355"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created category in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$category.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{defaultUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="No"/> + <argument name="description" value="Update Category Url Rewrite"/> + </actionGroup> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Assert category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByNewRequestPath"> + <argument name="redirectPath" value="{{defaultUrlRewrite.request_path}}" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!-- Assert Category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="{{defaultUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml new file mode 100644 index 0000000000000..be9fd1d83c8f1 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest.xml @@ -0,0 +1,61 @@ +<?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="AdminUpdateCategoryUrlRewriteAndAddPermanentRedirectTest"> + <annotations> + <stories value="Update URL rewrite"/> + <title value="Update Category URL Rewrites, permanent"/> + <description value="Login as Admin and update category UrlRewrite and add Permanent redirect type"/> + <testCaseId value="MC-5357"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created category in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$category.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{defaultUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="Permanent (301)"/> + <argument name="description" value="Update Category Url Rewrite"/> + </actionGroup> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!--Assert category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByNewRequestPath"> + <argument name="redirectPath" value="{{defaultUrlRewrite.request_path}}" /> + <argument name="redirectType" value="Permanent (301)" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!-- Assert category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="{{defaultUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..7e1b9acbc47ab --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,61 @@ +<?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="AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Update URL rewrite"/> + <title value="Update Category URL Rewrites, Temporary redirect type"/> + <description value="Login as Admin and update category UrlRewrite and add Temporary redirect type"/> + <testCaseId value="MC-5356"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created category in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$category.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{updateUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="Update Category Url Rewrite"/> + </actionGroup> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!--Assert category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByNewRequestPath"> + <argument name="redirectPath" value="{{updateUrlRewrite.request_path}}" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!-- Assert category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="{{updateUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml new file mode 100644 index 0000000000000..8339eb63abef1 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml @@ -0,0 +1,58 @@ +<?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="AdminUpdateCustomURLRewritesPermanentTest"> + <annotations> + <stories value="Update Custom URL Rewrites"/> + <title value="Update Custom URL Rewrites, permanent"/> + <description value="Test log in to URL Rewrites and Update Custom URL Rewrites, permanent"/> + <testCaseId value="MC-5353"/> + <severity value="CRITICAL"/> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultUrlRewrite" stepKey="urlRewrite"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{customPermanentUrlRewrite.request_path}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search default custom url rewrite in grid--> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="searchUrlRewrite"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Update default custom url rewrite as per requirement and verify AssertUrlRewriteSaveMessage--> + <actionGroup ref="AdminUpdateCustomUrlRewrite" stepKey="updateUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{customPermanentUrlRewrite.request_path}}"/> + <argument name="targetPath" value="{{customPermanentUrlRewrite.target_path}}"/> + <argument name="redirectTypeValue" value="{{customPermanentUrlRewrite.redirect_type_label}}"/> + <argument name="description" value="{{customPermanentUrlRewrite.description}}"/> + </actionGroup> + + <!--Search and verify updated AssertUrlRewriteInGrid--> + <actionGroup ref="AdminSearchByRequestPath" stepKey="verifyUpdatedUrlRewriteInGrid"> + <argument name="redirectPath" value="{{customPermanentUrlRewrite.request_path}}"/> + <argument name="redirectType" value="{{customPermanentUrlRewrite.redirect_type_label}}"/> + <argument name="targetPath" value="{{customPermanentUrlRewrite.target_path}}"/> + </actionGroup> + + <!--AssertUrlRewriteSuccessOutsideRedirect--> + <amOnPage url="{{StorefrontHomePage.url}}{{customPermanentUrlRewrite.request_path}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInCurrentUrl url="{{customPermanentUrlRewrite.target_path}}" stepKey="seeAssertUrlRewrite"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml new file mode 100644 index 0000000000000..07d578cbbeca4 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml @@ -0,0 +1,68 @@ +<?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="AdminUpdateCustomURLRewritesTemporaryTest"> + <annotations> + <stories value="Update Custom URL Rewrites"/> + <title value="Update Custom URL Rewrites, temporary"/> + <description value="Test log in to URL Rewrites and Update Custom URL Rewrites, temporary"/> + <testCaseId value="MC-5354"/> + <severity value="CRITICAL"/> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultUrlRewrite" stepKey="urlRewrite"/> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{customTemporaryUrlRewrite.request_path}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!--Search default custom url rewrite in grid--> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="searchUrlRewrite"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Update default custom url rewrite as per requirement and verify AssertUrlRewriteSaveMessage--> + <actionGroup ref="AdminUpdateCustomUrlRewrite" stepKey="updateUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{customTemporaryUrlRewrite.request_path}}"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + <argument name="redirectTypeValue" value="{{customTemporaryUrlRewrite.redirect_type_label}}"/> + <argument name="description" value="{{customTemporaryUrlRewrite.description}}"/> + </actionGroup> + + <!--Search and verify AssertUrlRewriteInGrid--> + <actionGroup ref="AdminSearchByRequestPath" stepKey="verifyUpdatedUrlRewriteInGrid"> + <argument name="redirectPath" value="{{customTemporaryUrlRewrite.request_path}}"/> + <argument name="redirectType" value="{{customTemporaryUrlRewrite.redirect_type_label}}"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- AssertUrlRewriteCustomSearchRedirect--> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="$$createProduct.name$$.html"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 0000000000000..ea370d8419583 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,49 @@ +<?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="AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="URL Rewrite"/> + <title value="Update Product URL Rewrites"/> + <description value="Login as Admin and update product UrlRewrite and add Temporary redirect type "/> + <testCaseId value="MC-5351"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created product in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="{{updateUrlRewrite.store}}"/> + <argument name="requestPath" value="{{updateUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="{{updateUrlRewrite.redirect_type_label}}"/> + <argument name="description" value="{{updateUrlRewrite.description}}"/> + </actionGroup> + + <!-- Assert product Url Rewrite in StoreFront --> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="assertProductUrlRewriteInStoreFront"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="{{updateUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml b/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml index 7d8151d270308..de8575178d06d 100644 --- a/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml +++ b/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml @@ -14,6 +14,8 @@ <argument name="id" xsi:type="string">urlrewriteGrid</argument> <argument name="dataSource" xsi:type="object">Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection</argument> <argument name="default_sort" xsi:type="string">url_rewrite_id</argument> + <!-- Add below argument to save session parameter in URL rewrite grid --> + <argument name="save_parameters_in_session" xsi:type="string">1</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" as="grid.columnSet" name="adminhtml.url_rewrite.grid.columnSet"> <arguments> diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 1c25ffd1e9ff7..c842d660a6176 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -76,6 +76,7 @@ public function resolve( $result = [ 'id' => $urlRewrite->getEntityId(), 'canonical_url' => $urlRewrite->getTargetPath(), + 'relative_url' => $urlRewrite->getTargetPath(), 'type' => $this->sanitizeType($urlRewrite->getEntityType()) ]; } diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index dae695c69a33c..5aea482a0fe02 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -1,16 +1,17 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `canonical_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.") - canonical_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") - type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") -} - type Query { urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") } +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.") + 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.") + type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") +} + enum UrlRewriteEntityTypeEnum { } diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminClickSaveButtonOnUserFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminClickSaveButtonOnUserFormActionGroup.xml new file mode 100644 index 0000000000000..e1edb16aba6ea --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminClickSaveButtonOnUserFormActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AdminClickSaveButtonOnUserFormActionGroup"> + <click selector="{{AdminNewUserFormSection.save}}" stepKey="saveNewUser"/> + <waitForPageLoad stepKey="waitForSaveResultLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml new file mode 100644 index 0000000000000..da08ac469b7c4 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml @@ -0,0 +1,45 @@ +<?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="AdminCreateRoleActionGroup"> + <arguments> + <argument name="restrictedRole"/> + <argument name="User"/> + </arguments> + <amOnPage url="{{AdminEditRolePage.url}}" stepKey="navigateToNewRole"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <fillField selector="{{AdminEditRoleInfoSection.roleName}}" userInput="{{User.name}}" stepKey="fillRoleName" /> + <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterPassword" /> + <click selector="{{AdminEditRoleInfoSection.roleResourcesTab}}" stepKey="clickRoleResourcesTab" /> + <waitForElementVisible selector="{{AdminEditRoleResourcesSection.roleScopes}}" stepKey="waitForScopeSelection" /> + <selectOption selector="{{AdminEditRoleResourcesSection.resourceAccess}}" userInput="0" stepKey="selectResourceAccessCustom"/> + <waitForElementVisible stepKey="waitForElementVisible" selector="{{AdminEditRoleInfoSection.blockName('restrictedRole')}}" time="30"/> + <click stepKey="clickContentBlockCheckbox" selector="{{AdminEditRoleInfoSection.blockName('restrictedRole')}}"/> + <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + </actionGroup> + <!--Create new role--> + <actionGroup name="AdminCreateRole"> + <arguments> + <argument name="role" type="string" defaultValue=""/> + <argument name="resource" type="string" defaultValue="All"/> + <argument name="scope" type="string" defaultValue="Custom"/> + <argument name="websites" type="string" defaultValue="Main Website"/> + </arguments> + <click selector="{{AdminCreateRoleSection.create}}" stepKey="clickToAddNewRole"/> + <fillField selector="{{AdminCreateRoleSection.name}}" userInput="{{role.name}}" stepKey="setRoleName"/> + <fillField stepKey="setPassword" selector="{{AdminCreateRoleSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> + <waitForPageLoad stepKey="waitForRoleResourcePage" time="5"/> + <click stepKey="checkSales" selector="//a[text()='Sales']"/> + <click selector="{{AdminCreateRoleSection.save}}" stepKey="clickToSaveRole"/> + <waitForPageLoad stepKey="waitForPageLoad" time="10"/> + <see userInput="You saved the role." stepKey="seeSuccessMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml index 9a0fa4a205799..5d51dcc610f78 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml @@ -10,23 +10,24 @@ <actionGroup name="AdminCreateUserActionGroup"> <arguments> <argument name="role"/> - <argument name="User" defaultValue="admin2"/> + <argument name="User" defaultValue="newAdmin"/> </arguments> - <amOnPage url="{{AdminEditUserPage.url}}" stepKey="navigateToNewUser"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> - <fillField selector="{{AdminEditUserSection.usernameTextField}}" userInput="{{admin2.username}}" stepKey="enterUserName" /> - <fillField selector="{{AdminEditUserSection.firstNameTextField}}" userInput="{{admin2.firstName}}" stepKey="enterFirstName" /> - <fillField selector="{{AdminEditUserSection.lastNameTextField}}" userInput="{{admin2.lastName}}" stepKey="enterLastName" /> - <fillField selector="{{AdminEditUserSection.emailTextField}}" userInput="{{admin2.username}}@magento.com" stepKey="enterEmail" /> - <fillField selector="{{AdminEditUserSection.passwordTextField}}" userInput="{{admin2.password}}" stepKey="enterPassword" /> - <fillField selector="{{AdminEditUserSection.pwConfirmationTextField}}" userInput="{{admin2.password}}" stepKey="confirmPassword" /> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="amOnAdminUsersPage"/> + <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> + <click selector="{{AdminCreateUserSection.create}}" stepKey="clickToCreateNewUser"/> + <fillField selector="{{AdminEditUserSection.usernameTextField}}" userInput="{{newAdmin.username}}" stepKey="enterUserName" /> + <fillField selector="{{AdminEditUserSection.firstNameTextField}}" userInput="{{newAdmin.firstName}}" stepKey="enterFirstName" /> + <fillField selector="{{AdminEditUserSection.lastNameTextField}}" userInput="{{newAdmin.lastName}}" stepKey="enterLastName" /> + <fillField selector="{{AdminEditUserSection.emailTextField}}" userInput="{{newAdmin.username}}@magento.com" stepKey="enterEmail" /> + <fillField selector="{{AdminEditUserSection.passwordTextField}}" userInput="{{newAdmin.password}}" stepKey="enterPassword" /> + <fillField selector="{{AdminEditUserSection.pwConfirmationTextField}}" userInput="{{newAdmin.password}}" stepKey="confirmPassword" /> <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterCurrentPassword" /> <scrollToTopOfPage stepKey="scrollToTopOfPage" /> <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRole" /> - <fillField selector="{{AdminEditUserRoleSection.roleNameFilterTextField}}" userInput="{{role.name}}" stepKey="filterRole" /> - <click selector="{{AdminEditUserRoleSection.searchButton}}" stepKey="clickSearch" /> + <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="{{role.name}}" stepKey="filterRole" /> + <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearch" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> - <click selector="{{AdminEditUserRoleSection.searchResultFirstRow}}" stepKey="selectRole" /> + <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="selectRole" /> <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveUser" /> <waitForPageLoad stepKey="waitForPageLoad2" /> <see userInput="You saved the user." stepKey="seeSuccessMessage" /> @@ -38,7 +39,7 @@ <argument name="role"/> <argument name="user" defaultValue="newAdmin"/> </arguments> - <amOnPage url="{{AdminEditUserPage.url}}" stepKey="navigateToNewUser"/> + <amOnPage url="{{AdminNewUserPage.url}}" stepKey="navigateToNewUser"/> <waitForPageLoad stepKey="waitForUsersPage" /> <fillField selector="{{AdminCreateUserSection.usernameTextField}}" userInput="{{user.username}}" stepKey="enterUserName" /> <fillField selector="{{AdminCreateUserSection.firstNameTextField}}" userInput="{{user.firstName}}" stepKey="enterFirstName" /> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml new file mode 100644 index 0000000000000..813e22df227c8 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml @@ -0,0 +1,24 @@ +<?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="AdminDeleteCreatedRoleActionGroup"> + <arguments> + <argument name="role" defaultValue=""/> + </arguments> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="amOnAdminUsersPage"/> + <waitForPageLoad stepKey="waitForUserRolePageLoad"/> + <click stepKey="clickToAddNewRole" selector="{{AdminDeleteRoleSection.role(role.name)}}"/> + <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <click stepKey="clickToDeleteRole" selector="{{AdminDeleteRoleSection.delete}}"/> + <waitForElementVisible stepKey="wait" selector="{{AdminDeleteRoleSection.confirm}}" time="30"/> + <click stepKey="clickToConfirm" selector="{{AdminDeleteRoleSection.confirm}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see stepKey="seeSuccessMessage" userInput="You deleted the role."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml index 7f1ed3be1ca57..74124f366a54b 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml @@ -13,6 +13,7 @@ </arguments> <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> <click stepKey="openTheUser" selector="{{AdminDeleteUserSection.role(user.username)}}"/> + <waitForPageLoad stepKey="waitForSingleUserPageToLoad" /> <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml index 70c0a772ec341..9b7342e531b66 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml @@ -7,6 +7,21 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteUserActionGroup"> + <arguments> + <argument name="user"/> + </arguments> + <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> + <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> + <click stepKey="openTheUser" selector="{{AdminDeleteUserSection.role(user.name)}}"/> + <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="clickToDeleteRole" selector="{{AdminDeleteUserSection.delete}}"/> + <waitForElementVisible stepKey="wait" selector="{{AdminDeleteRoleSection.confirm}}" time="30"/> + <click stepKey="clickToConfirm" selector="{{AdminDeleteUserSection.confirm}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see stepKey="seeDeleteMessageForUser" userInput="You deleted the user."/> + </actionGroup> <actionGroup name="AdminDeleteCustomUserActionGroup"> <arguments> <argument name="user"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillForgotPasswordFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillForgotPasswordFormActionGroup.xml new file mode 100644 index 0000000000000..01be51e72ec6d --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillForgotPasswordFormActionGroup.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="AdminFillForgotPasswordFormActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + + <fillField selector="{{AdminForgotPasswordFormSection.email}}" userInput="{{email}}" stepKey="fillAdminEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml new file mode 100644 index 0000000000000..87bf1e003931a --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.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="AdminFillNewUserFormRequiredFieldsActionGroup"> + <arguments> + <argument name="user" type="entity" /> + </arguments> + <fillField selector="{{AdminNewUserFormSection.username}}" userInput="{{user.username}}" stepKey="fillUser"/> + <fillField selector="{{AdminNewUserFormSection.firstname}}" userInput="{{user.firstname}}" stepKey="fillFirstName"/> + <fillField selector="{{AdminNewUserFormSection.lastname}}" userInput="{{user.lastname}}" stepKey="fillLastName"/> + <fillField selector="{{AdminNewUserFormSection.email}}" userInput="{{user.email}}" stepKey="fillEmail"/> + <fillField selector="{{AdminNewUserFormSection.password}}" userInput="{{user.password}}" stepKey="fillPassword"/> + <fillField selector="{{AdminNewUserFormSection.passwordConfirmation}}" userInput="{{user.password_confirmation}}" stepKey="fillPasswordConfirmation"/> + <fillField selector="{{AdminNewUserFormSection.currentPassword}}" userInput="{{user.current_password}}" stepKey="fillCurrentUserPassword"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewUserFormSection.userRoleTab}}" stepKey="openUserRoleTab"/> + <waitForPageLoad stepKey="waitForUserRoleTabOpened" /> + <click selector="{{AdminNewUserFormSection.resetFilter}}" stepKey="resetGridFilter" /> + <waitForPageLoad stepKey="waitForFiltersReset" /> + <fillField userInput="{{user.role}}" selector="{{AdminNewUserFormSection.roleFilterField}}" stepKey="fillRoleFilterField" /> + <click selector="{{AdminNewUserFormSection.search}}" stepKey="clickSearchButton" /> + <waitForPageLoad stepKey="waitForFiltersApplied" /> + <checkOption selector="{{AdminNewUserFormSection.roleRadiobutton(user.role)}}" stepKey="assignRole"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenForgotPasswordPageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenForgotPasswordPageActionGroup.xml new file mode 100644 index 0000000000000..fa17c5a7f8b76 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenForgotPasswordPageActionGroup.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="AdminOpenForgotPasswordPageActionGroup"> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <waitForPageLoad stepKey="waitForAdminLoginPage"/> + <click stepKey="clickForgotPasswordLink" selector="{{AdminLoginFormSection.forgotPasswordLink}}"/> + <waitForPageLoad stepKey="waitForAdminForgotPasswordPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenNewUserPageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenNewUserPageActionGroup.xml new file mode 100644 index 0000000000000..67aef9379faa8 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenNewUserPageActionGroup.xml @@ -0,0 +1,14 @@ +<?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="AdminOpenNewUserPageActionGroup"> + <amOnPage url="{{AdminNewUserPage.url}}" stepKey="amOnNewAdminUserPage"/> + <waitForPageLoad stepKey="waitForNewAdminUserPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSubmitForgotPasswordFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSubmitForgotPasswordFormActionGroup.xml new file mode 100644 index 0000000000000..198bc713093ea --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSubmitForgotPasswordFormActionGroup.xml @@ -0,0 +1,13 @@ +<?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="AdminSubmitForgotPasswordFormActionGroup"> + <click selector="{{AdminForgotPasswordFormSection.retrievePasswordButton}}" stepKey="clickOnRetrievePasswordButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserSaveMessageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserSaveMessageActionGroup.xml new file mode 100644 index 0000000000000..db4f0a89348a9 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserSaveMessageActionGroup.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="AssertAdminUserSaveMessageActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="You saved the user." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <waitForElementVisible selector="{{AdminUserFormMessagesSection.messageByType(messageType)}}" stepKey="waitForMessage" /> + <see userInput="{{message}}" selector="{{AdminUserFormMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/User/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..e8b7d2aa8e047 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?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="AdminMenuSystemPermissionsAllUsers"> + <data key="pageTitle">Users</data> + <data key="title">All Users</data> + <data key="dataUiId">magento-user-system-acl-users</data> + </entity> + <entity name="AdminMenuSystemPermissionsLockedUsers"> + <data key="pageTitle">Locked Users</data> + <data key="title">Locked Users</data> + <data key="dataUiId">magento-user-system-acl-locks</data> + </entity> + <entity name="AdminMenuSystemOtherSettingsManageEncryptionKey"> + <data key="pageTitle">Encryption Key</data> + <data key="title">Manage Encryption Key</data> + <data key="dataUiId">magento-encryptionkey-system-crypt-key</data> + </entity> + <entity name="AdminMenuSystemPermissionsUserRoles"> + <data key="pageTitle">Roles</data> + <data key="title">User Roles</data> + <data key="dataUiId">magento-user-system-acl-roles</data> + </entity> +</entities> diff --git a/app/code/Magento/User/Test/Mftf/Data/UserData.xml b/app/code/Magento/User/Test/Mftf/Data/UserData.xml index 80c1cc3022964..e665736ae28f1 100644 --- a/app/code/Magento/User/Test/Mftf/Data/UserData.xml +++ b/app/code/Magento/User/Test/Mftf/Data/UserData.xml @@ -8,6 +8,48 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultAdminUser" type="user"> + <data key="username">{{_ENV.MAGENTO_ADMIN_USERNAME}}</data> + <data key="password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + </entity> + <entity name="AdminUserWrongCredentials"> + <data key="username" unique="suffix">username_</data> + <data key="password" unique="suffix">password_</data> + </entity> + <entity name="NewAdminUser" type="user"> + <data key="username" unique="suffix">admin</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="email" unique="prefix">admin@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="interface_local_label">English (United States)</data> + <data key="is_active">true</data> + <data key="is_active_label">Active</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="role">Administrators</data> + <array key="roles"> + <item>1</item> + </array> + </entity> + <entity name="NewAdminUserWrongCurrentPassword" type="user"> + <data key="username" unique="suffix">admin</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="email" unique="prefix">admin@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="interface_local_label">English (United States)</data> + <data key="is_active">true</data> + <data key="is_active_label">Active</data> + <data key="current_password" unique="suffix">password_</data> + <data key="role">Administrators</data> + <array key="roles"> + <item>1</item> + </array> + </entity> <entity name="admin" type="user"> <data key="email">admin@magento.com</data> <data key="password">admin123</data> diff --git a/app/code/Magento/User/Test/Mftf/Page/AdminNewUserPage.xml b/app/code/Magento/User/Test/Mftf/Page/AdminNewUserPage.xml new file mode 100644 index 0000000000000..6de0945793447 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Page/AdminNewUserPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminNewUserPage" url="admin/user/new" area="admin" module="Magento_User"> + <section name="AdminNewUserFormSection" /> + <section name="AdminNewUserFormMessagesSection" /> + </page> +</pages> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml similarity index 83% rename from app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml rename to app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml index 1158f471d51f0..7dd313a2ba897 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreateRoleSection"> <element name="create" type="button" selector="#add"/> <element name="name" type="button" selector="#role_name"/> @@ -21,4 +23,4 @@ <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml new file mode 100644 index 0000000000000..1b55d09d0597e --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminDeleteRoleSection"> + <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> + <element name="current_pass" type="button" selector="#current_password"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml index e30a545649d12..57659e1aff075 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -17,5 +17,6 @@ <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> + <element name="blockName" type="checkbox" selector="//*[text()='{{var}}']//*[@class='jstree-checkbox']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml index 5b866b45e2fbe..64068a0a5ef58 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml @@ -5,8 +5,12 @@ * 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"> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEditUserSection"> + <element name="system" type="input" selector="#menu-magento-backend-system"/> + <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> + <element name="create" type="input" selector="#add"/> <element name="usernameTextField" type="input" selector="#user_username"/> <element name="firstNameTextField" type="input" selector="#user_firstname"/> <element name="lastNameTextField" type="input" selector="#user_lastname"/> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml new file mode 100644 index 0000000000000..9b030b216ce2c --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml @@ -0,0 +1,30 @@ +<?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="AdminNewUserFormSection"> + <element name="save" type="button" selector=".page-main-actions #save"/> + + <element name="userInfoTab" type="button" selector="#page_tabs_main_section"/> + <element name="username" type="input" selector="#page_tabs_main_section_content input[name='username']"/> + <element name="firstname" type="input" selector="#page_tabs_main_section_content input[name='firstname']"/> + <element name="lastname" type="input" selector="#page_tabs_main_section_content input[name='lastname']"/> + <element name="email" type="input" selector="#page_tabs_main_section_content input[name='email']"/> + <element name="password" type="input" selector="#page_tabs_main_section_content input[name='password']"/> + <element name="passwordConfirmation" type="input" selector="#page_tabs_main_section_content input[name='password_confirmation']"/> + <element name="interfaceLocale" type="select" selector="#page_tabs_main_section_content select[name='interface_locale']"/> + <element name="currentPassword" type="input" selector="#page_tabs_main_section_content input[name='current_password']"/> + + <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> + <element name="search" type="button" selector="#page_tabs_roles_section_content #permissionsUserRolesGrid [data-action='grid-filter-apply']" /> + <element name="resetFilter" type="button" selector="#page_tabs_roles_section_content #permissionsUserRolesGrid [data-action='grid-filter-reset']" /> + <element name="roleFilterField" type="input" selector="#page_tabs_roles_section_content #permissionsUserRolesGrid input[name='role_name']" /> + <element name="roleRadiobutton" type="radio" selector="//table[@id='permissionsUserRolesGrid_table']//tr[./td[contains(@class, 'col-role_name') and contains(., '{{roleName}}')]]//input[@name='roles[]']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml index 6db6858500342..8413081237fd1 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml @@ -14,4 +14,11 @@ <element name="roleNameInFirstRow" type="text" selector=".col-role_name"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> </section> + + <section name="AdminDeleteRoleSection"> + <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> + <element name="current_pass" type="button" selector="#current_password"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminUserFormMessagesSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserFormMessagesSection.xml new file mode 100644 index 0000000000000..ec4f4d8bf3ad7 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserFormMessagesSection.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="AdminUserFormMessagesSection"> + <element name="messageByType" type="block" selector="#messages .message-{{messageType}}" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml index f429c390efe6b..c21a8b875e95b 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml @@ -14,4 +14,11 @@ <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> <element name="successMessage" type="text" selector=".message-success"/> </section> + + <section name="AdminDeleteUserSection"> + <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> + <element name="password" selector="#user_current_password" type="input"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml new file mode 100644 index 0000000000000..4b48c65a18994 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml @@ -0,0 +1,43 @@ +<?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="AdminResetUserPasswordFailedTest"> + <annotations> + <features value="User"/> + <title value="Admin user should not be able to trigger the password reset procedure twice"/> + <description value="Admin user should not be able to trigger the password reset procedure twice"/> + <testCaseId value="MC-14389" /> + <group value="security"/> + <group value="mtf_migrated"/> + </annotations> + + <!-- First attempt to reset password --> + <actionGroup ref="AdminOpenForgotPasswordPageActionGroup" stepKey="openAdminForgotPasswordPage1"/> + <actionGroup ref="AdminFillForgotPasswordFormActionGroup" stepKey="fillAdminForgotPasswordForm1"> + <argument name="email" value="customer@example.com"/> + </actionGroup> + <actionGroup ref="AdminSubmitForgotPasswordFormActionGroup" stepKey="submitAdminForgotPasswordForm1"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeSuccessMessage"> + <argument name="messageType" value="success"/> + <argument name="message" value="We'll email you a link to reset your password."/> + </actionGroup> + + <!-- Second attempt to reset password --> + <actionGroup ref="AdminOpenForgotPasswordPageActionGroup" stepKey="openAdminForgotPasswordPage2"/> + <actionGroup ref="AdminFillForgotPasswordFormActionGroup" stepKey="fillAdminForgotPasswordForm2"> + <argument name="email" value="customer@example.com"/> + </actionGroup> + <actionGroup ref="AdminSubmitForgotPasswordFormActionGroup" stepKey="submitAdminForgotPasswordForm2"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeErrorMessage"> + <argument name="messageType" value="error"/> + <argument name="message" value="We received too many requests for password resets. Please wait and try again later or contact hello@example.com."/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml new file mode 100644 index 0000000000000..b899320403d71 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemAllUsersNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system all users navigate menu test"/> + <description value="Admin should be able to navigate to System > All Users"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14123"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemAllUsersPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemPermissionsAllUsers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemPermissionsAllUsers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml new file mode 100644 index 0000000000000..aea46f3273157 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemLockedUsersNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system locked users navigate menu test"/> + <description value="Admin should be able to navigate to System > Locked Users"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14121"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemLockedUsersPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemPermissionsLockedUsers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemPermissionsLockedUsers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml new file mode 100644 index 0000000000000..f8013a54058c3 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemManageEncryptionKeyNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system manage encryption key navigate menu test"/> + <description value="Admin should be able to navigate to System > Manage Encryption Key"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14122"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToManageEncryptionKeyPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsManageEncryptionKey.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemOtherSettingsManageEncryptionKey.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml new file mode 100644 index 0000000000000..c4052a7f4219c --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemUserRolesNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system user roles navigate menu test"/> + <description value="Admin should be able to navigate to System > User Roles"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14124"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemUserRolesPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemPermissionsUserRoles.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemPermissionsUserRoles.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml b/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml index a4bc9aa5ed48b..8289b3e730d5d 100644 --- a/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml +++ b/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml @@ -16,6 +16,7 @@ <argument name="default_sort" xsi:type="string">username</argument> <argument name="default_dir" xsi:type="string">asc</argument> <argument name="grid_url" xsi:type="url" path="*/*/roleGrid"/> + <argument name="save_parameters_in_session" xsi:type="boolean">true</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" as="grid.columnSet" name="permission.user.grid.columnSet"> <arguments> diff --git a/app/code/Magento/Variable/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Variable/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..e094239767486 --- /dev/null +++ b/app/code/Magento/Variable/Test/Mftf/Data/AdminMenuData.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="AdminMenuSystemOtherSettingsCustomVariables"> + <data key="pageTitle">Custom Variables</data> + <data key="title">Custom Variables</data> + <data key="dataUiId">magento-variable-system-variable</data> +</entity> +</entities> diff --git a/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml b/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml new file mode 100644 index 0000000000000..74446cf601348 --- /dev/null +++ b/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminSystemCustomVariablesNavigateMenuTest"> + <annotations> + <features value="Variable"/> + <stories value="Menu Navigation"/> + <title value="Admin system custom variables navigate menu test"/> + <description value="Admin should be able to navigate to System > Custom Variables"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14126"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemCustomVariablesPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsCustomVariables.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemOtherSettingsCustomVariables.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Variable/etc/di.xml b/app/code/Magento/Variable/etc/di.xml index f0a24e89ef8d4..41759e1f1582b 100644 --- a/app/code/Magento/Variable/etc/di.xml +++ b/app/code/Magento/Variable/etc/di.xml @@ -42,6 +42,7 @@ <item name="general/store_information/merchant_vat_number" xsi:type="string">1</item> </item> </argument> + <argument name="configStructure" xsi:type="object">Magento\Config\Model\Config\Structure\Proxy</argument> </arguments> </type> -</config> \ No newline at end of file +</config> diff --git a/app/code/Magento/Variable/view/adminhtml/web/variables.js b/app/code/Magento/Variable/view/adminhtml/web/variables.js index 47f027f27102d..bf8bfbc570ce2 100644 --- a/app/code/Magento/Variable/view/adminhtml/web/variables.js +++ b/app/code/Magento/Variable/view/adminhtml/web/variables.js @@ -16,7 +16,8 @@ define([ 'Magento_Variable/js/custom-directive-generator', 'Magento_Ui/js/lib/spinner', 'jquery/ui', - 'prototype' + 'prototype', + 'mage/adminhtml/tools' ], function (jQuery, notification, $t, wysiwyg, registry, mageApply, utils, configGenerator, customGenerator, loader) { 'use strict'; diff --git a/app/code/Magento/Vault/view/frontend/web/template/payment/form.html b/app/code/Magento/Vault/view/frontend/web/template/payment/form.html index b5593626fb15c..5f32281686a65 100644 --- a/app/code/Magento/Vault/view/frontend/web/template/payment/form.html +++ b/app/code/Magento/Vault/view/frontend/web/template/payment/form.html @@ -19,7 +19,8 @@ <img data-bind="attr: { 'src': getIcons(getCardType()).url, 'width': getIcons(getCardType()).width, - 'height': getIcons(getCardType()).height + 'height': getIcons(getCardType()).height, + 'alt': getIcons(getCardType()).title }" class="payment-icon"> <span translate="'ending'"></span> <span text="getMaskedCard()"></span> diff --git a/app/code/Magento/VaultGraphQl/Model/Resolver/DeletePaymentToken.php b/app/code/Magento/VaultGraphQl/Model/Resolver/DeletePaymentToken.php new file mode 100644 index 0000000000000..cbdbbdcf010b6 --- /dev/null +++ b/app/code/Magento/VaultGraphQl/Model/Resolver/DeletePaymentToken.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\VaultGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Vault\Api\PaymentTokenManagementInterface; +use Magento\Vault\Api\PaymentTokenRepositoryInterface; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; + +/** + * Delete Payment Token resolver, used for GraphQL mutation processing. + */ +class DeletePaymentToken implements ResolverInterface +{ + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var PaymentTokenManagementInterface + */ + private $paymentTokenManagement; + + /** + * @var PaymentTokenRepositoryInterface + */ + private $paymentTokenRepository; + + /** + * @param GetCustomer $getCustomer + * @param PaymentTokenManagementInterface $paymentTokenManagement + * @param PaymentTokenRepositoryInterface $paymentTokenRepository + */ + public function __construct( + GetCustomer $getCustomer, + PaymentTokenManagementInterface $paymentTokenManagement, + PaymentTokenRepositoryInterface $paymentTokenRepository + ) { + $this->getCustomer = $getCustomer; + $this->paymentTokenManagement = $paymentTokenManagement; + $this->paymentTokenRepository = $paymentTokenRepository; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['public_hash'])) { + throw new GraphQlInputException(__('Specify the "public_hash" value.')); + } + + $customer = $this->getCustomer->execute($context); + + $token = $this->paymentTokenManagement->getByPublicHash($args['public_hash'], $customer->getId()); + if (!$token) { + throw new GraphQlNoSuchEntityException( + __('Could not find a token using public hash: %1', $args['public_hash']) + ); + } + + return ['result' => $this->paymentTokenRepository->delete($token)]; + } +} diff --git a/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php b/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php new file mode 100644 index 0000000000000..1563eaedf6b9b --- /dev/null +++ b/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\VaultGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; + +/** + * Customers Payment Tokens resolver, used for GraphQL request processing. + */ +class PaymentTokens implements ResolverInterface +{ + /** + * @var PaymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @param PaymentTokenManagement $paymentTokenManagement + * @param GetCustomer $getCustomer + */ + public function __construct( + PaymentTokenManagement $paymentTokenManagement, + GetCustomer $getCustomer + ) { + $this->paymentTokenManagement = $paymentTokenManagement; + $this->getCustomer = $getCustomer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customer = $this->getCustomer->execute($context); + + $tokens = $this->paymentTokenManagement->getVisibleAvailableTokens($customer->getId()); + $result = []; + + foreach ($tokens as $token) { + $result[] = [ + 'public_hash' => $token->getPublicHash(), + 'payment_method_code' => $token->getPaymentMethodCode(), + 'type' => $token->getType(), + 'details' => $token->getTokenDetails(), + ]; + } + return ['items' => $result]; + } +} diff --git a/app/code/Magento/VaultGraphQl/README.md b/app/code/Magento/VaultGraphQl/README.md new file mode 100644 index 0000000000000..afcb1d83f2771 --- /dev/null +++ b/app/code/Magento/VaultGraphQl/README.md @@ -0,0 +1,5 @@ +# VaultGraphQl + +**VaultGraphQl** provides type and resolver information for the GraphQl module +to generate Vault (stored payment information) information endpoints. This module also +provides mutations for modifying a payment token. diff --git a/app/code/Magento/VaultGraphQl/composer.json b/app/code/Magento/VaultGraphQl/composer.json new file mode 100644 index 0000000000000..455d24bfc11f8 --- /dev/null +++ b/app/code/Magento/VaultGraphQl/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-vault-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-vault": "*", + "magento/module-customer-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\VaultGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/VaultGraphQl/etc/module.xml b/app/code/Magento/VaultGraphQl/etc/module.xml new file mode 100644 index 0000000000000..f821d9fa67041 --- /dev/null +++ b/app/code/Magento/VaultGraphQl/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_VaultGraphQl"/> +</config> diff --git a/app/code/Magento/VaultGraphQl/etc/schema.graphqls b/app/code/Magento/VaultGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..cdaeced027f6f --- /dev/null +++ b/app/code/Magento/VaultGraphQl/etc/schema.graphqls @@ -0,0 +1,31 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + deletePaymentToken(public_hash: String!): DeletePaymentTokenOutput @resolver(class: "\\Magento\\VaultGraphQl\\Model\\Resolver\\DeletePaymentToken") @doc(description:"Delete a customer payment token") +} + +type DeletePaymentTokenOutput { + result: Boolean! + customerPaymentTokens: CustomerPaymentTokens @resolver(class: "\\Magento\\VaultGraphQl\\Model\\Resolver\\PaymentTokens") +} + +type Query { + customerPaymentTokens: CustomerPaymentTokens @doc(description: "Return a list of customer payment tokens") @resolver(class: "\\Magento\\VaultGraphQl\\Model\\Resolver\\PaymentTokens") +} + +type CustomerPaymentTokens @resolver(class: "\\Magento\\VaultGraphQl\\Model\\Resolver\\PaymentTokens") { + items: [PaymentToken]! @doc(description: "An array of payment tokens") +} + +type PaymentToken @doc(description: "The stored payment method available to the customer") { + public_hash: String! @doc(description: "The public hash of the token") + payment_method_code: String! @doc(description: "The payment method code associated with the token") + type: PaymentTokenTypeEnum! + details: String @doc(description: "Stored account details") +} + +enum PaymentTokenTypeEnum @doc(description: "The list of available payment token types") { + card + account +} diff --git a/app/code/Magento/VaultGraphQl/registration.php b/app/code/Magento/VaultGraphQl/registration.php new file mode 100644 index 0000000000000..3f48c00f0709e --- /dev/null +++ b/app/code/Magento/VaultGraphQl/registration.php @@ -0,0 +1,10 @@ +<?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_VaultGraphQl', __DIR__); diff --git a/app/code/Magento/Webapi/Model/Config/ClassReflector.php b/app/code/Magento/Webapi/Model/Config/ClassReflector.php index 7ce94c9bc6eeb..6748319f9f482 100644 --- a/app/code/Magento/Webapi/Model/Config/ClassReflector.php +++ b/app/code/Magento/Webapi/Model/Config/ClassReflector.php @@ -129,8 +129,8 @@ protected function extractMethodDescription(\Zend\Code\Reflection\MethodReflecti $docBlock = $methodReflection->getDocBlock(); if (!$docBlock) { throw new \LogicException( - 'The docBlock of the method '. - $method->getDeclaringClass()->getName() . '::' . $method->getName() . ' is empty.' + 'The docBlock of the method ' . + $method->getDeclaringClass()->getName() . '::' . $method->getName() . ' is empty.' ); } return $this->_typeProcessor->getDescription($docBlock); diff --git a/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php b/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php index 39e7fa418a35c..2a6cac09c22f7 100644 --- a/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php +++ b/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php @@ -67,7 +67,8 @@ public function read($scope = null) CommunicationConfig::HANDLER_TYPE => $serviceClass, CommunicationConfig::HANDLER_METHOD => $serviceMethod, ], - ] + ], + false ); $rewriteTopicParams = [ CommunicationConfig::TOPIC_IS_SYNCHRONOUS => false, diff --git a/app/code/Magento/WebapiAsync/Model/Config.php b/app/code/Magento/WebapiAsync/Model/Config.php index 92343027adcf6..16c24643ba355 100644 --- a/app/code/Magento/WebapiAsync/Model/Config.php +++ b/app/code/Magento/WebapiAsync/Model/Config.php @@ -15,6 +15,9 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Webapi\Model\Config\Converter; +/** + * Class for accessing to Webapi_Async configuration. + */ class Config implements \Magento\AsynchronousOperations\Model\ConfigInterface { /** @@ -55,7 +58,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getServices() { @@ -73,26 +76,30 @@ public function getServices() } /** - * {@inheritdoc} + * @inheritdoc */ public function getTopicName($routeUrl, $httpMethod) { $services = $this->getServices(); - $topicName = $this->generateTopicNameByRouteData( + $lookupKey = $this->generateLookupKeyByRouteData( $routeUrl, $httpMethod ); - if (array_key_exists($topicName, $services) === false) { + if (array_key_exists($lookupKey, $services) === false) { throw new LocalizedException( - __('WebapiAsync config for "%topicName" does not exist.', ['topicName' => $topicName]) + __('WebapiAsync config for "%lookupKey" does not exist.', ['lookupKey' => $lookupKey]) ); } - return $services[$topicName][self::SERVICE_PARAM_KEY_TOPIC]; + return $services[$lookupKey][self::SERVICE_PARAM_KEY_TOPIC]; } /** + * Generate topic data for all defined services + * + * Topic data is indexed by a lookup key that is derived from route data + * * @return array */ private function generateTopicsDataFromWebapiConfig() @@ -105,11 +112,18 @@ private function generateTopicsDataFromWebapiConfig() $serviceInterface = $httpMethodData[Converter::KEY_SERVICE][Converter::KEY_SERVICE_CLASS]; $serviceMethod = $httpMethodData[Converter::KEY_SERVICE][Converter::KEY_SERVICE_METHOD]; - $topicName = $this->generateTopicNameByRouteData( + $lookupKey = $this->generateLookupKeyByRouteData( $routeUrl, $httpMethod ); - $services[$topicName] = [ + + $topicName = $this->generateTopicNameFromService( + $serviceInterface, + $serviceMethod, + $httpMethod + ); + + $services[$lookupKey] = [ self::SERVICE_PARAM_KEY_INTERFACE => $serviceInterface, self::SERVICE_PARAM_KEY_METHOD => $serviceMethod, self::SERVICE_PARAM_KEY_TOPIC => $topicName, @@ -122,7 +136,7 @@ private function generateTopicsDataFromWebapiConfig() } /** - * Generate topic name based on service type and method name. + * Generate lookup key name based on route and method * * Perform the following conversion: * self::TOPIC_PREFIX + /V1/products + POST => async.V1.products.POST @@ -131,19 +145,39 @@ private function generateTopicsDataFromWebapiConfig() * @param string $httpMethod * @return string */ - private function generateTopicNameByRouteData($routeUrl, $httpMethod) + private function generateLookupKeyByRouteData($routeUrl, $httpMethod) { - return self::TOPIC_PREFIX . $this->generateTopicName($routeUrl, $httpMethod, '/', false); + return self::TOPIC_PREFIX . $this->generateKey($routeUrl, $httpMethod, '/', false); } /** + * Generate topic name based on service type and method name. + * + * Perform the following conversion: + * self::TOPIC_PREFIX + Magento\Catalog\Api\ProductRepositoryInterface + save + POST + * => async.magento.catalog.api.productrepositoryinterface.save.POST + * + * @param string $serviceInterface + * @param string $serviceMethod + * @param string $httpMethod + * @return string + */ + private function generateTopicNameFromService($serviceInterface, $serviceMethod, $httpMethod) + { + $typeName = strtolower(sprintf('%s.%s', $serviceInterface, $serviceMethod)); + return strtolower(self::TOPIC_PREFIX . $this->generateKey($typeName, $httpMethod, '\\', false)); + } + + /** + * Join and simplify input type and method into a string that can be used as an array key + * * @param string $typeName * @param string $methodName * @param string $delimiter * @param bool $lcfirst * @return string */ - private function generateTopicName($typeName, $methodName, $delimiter = '\\', $lcfirst = true) + private function generateKey($typeName, $methodName, $delimiter = '\\', $lcfirst = true) { $parts = explode($delimiter, ltrim($typeName, $delimiter)); foreach ($parts as &$part) { diff --git a/app/code/Magento/WebapiAsync/Plugin/Cache/Webapi.php b/app/code/Magento/WebapiAsync/Plugin/Cache/Webapi.php new file mode 100644 index 0000000000000..ecc929b204843 --- /dev/null +++ b/app/code/Magento/WebapiAsync/Plugin/Cache/Webapi.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\WebapiAsync\Plugin\Cache; + +use Magento\WebapiAsync\Controller\Rest\AsynchronousSchemaRequestProcessor; +use Magento\Framework\Webapi\Rest\Request; + +/** + * Class Webapi + */ +class Webapi +{ + /** + * Cache key for Async Routes + */ + const ASYNC_ROUTES_CONFIG_CACHE_ID = 'async-routes-services-config'; + + /** + * @var AsynchronousSchemaRequestProcessor + */ + private $asynchronousSchemaRequestProcessor; + + /** + * @var \Magento\Framework\Webapi\Rest\Request + */ + private $request; + + /** + * ServiceMetadata constructor. + * + * @param Request $request + * @param AsynchronousSchemaRequestProcessor $asynchronousSchemaRequestProcessor + */ + public function __construct( + \Magento\Framework\Webapi\Rest\Request $request, + AsynchronousSchemaRequestProcessor $asynchronousSchemaRequestProcessor + ) { + $this->request = $request; + $this->asynchronousSchemaRequestProcessor = $asynchronousSchemaRequestProcessor; + } + + /** + * Change identifier in case if Async request before cache load + * + * @param \Magento\Webapi\Model\Cache\Type\Webapi $subject + * @param string $identifier + * @return null|string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeLoad(\Magento\Webapi\Model\Cache\Type\Webapi $subject, $identifier) + { + if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request) + && $identifier === \Magento\Webapi\Model\ServiceMetadata::ROUTES_CONFIG_CACHE_ID) { + return self::ASYNC_ROUTES_CONFIG_CACHE_ID; + } + return null; + } + + /** + * Change identifier in case if Async request before cache save + * + * @param \Magento\Webapi\Model\Cache\Type\Webapi $subject + * @param string $data + * @param string $identifier + * @param array $tags + * @param int|bool|null $lifeTime + * @return array|null + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave( + \Magento\Webapi\Model\Cache\Type\Webapi $subject, + $data, + $identifier, + array $tags = [], + $lifeTime = null + ) { + if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request) + && $identifier === \Magento\Webapi\Model\ServiceMetadata::ROUTES_CONFIG_CACHE_ID) { + return [$data, self::ASYNC_ROUTES_CONFIG_CACHE_ID, $tags, $lifeTime]; + } + return null; + } + + /** + * Change identifier in case if Async request before remove cache + * + * @param \Magento\Webapi\Model\Cache\Type\Webapi $subject + * @param string $identifier + * @return null|string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeRemove(\Magento\Webapi\Model\Cache\Type\Webapi $subject, $identifier) + { + if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request) + && $identifier === \Magento\Webapi\Model\ServiceMetadata::ROUTES_CONFIG_CACHE_ID) { + return self::ASYNC_ROUTES_CONFIG_CACHE_ID; + } + return null; + } +} diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Model/ConfigTest.php b/app/code/Magento/WebapiAsync/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..47b75b2057316 --- /dev/null +++ b/app/code/Magento/WebapiAsync/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\WebapiAsync\Test\Unit\Model; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Webapi\Model\Cache\Type\Webapi; +use Magento\Webapi\Model\Config as WebapiConfig; +use Magento\WebapiAsync\Model\Config; +use Magento\Webapi\Model\Config\Converter; + +class ConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Config + */ + private $config; + + /** + * @var Webapi|\PHPUnit_Framework_MockObject_MockObject + */ + private $webapiCacheMock; + + /** + * @var WebapiConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializerMock; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->webapiCacheMock = $this->createMock(\Magento\Webapi\Model\Cache\Type\Webapi::class); + $this->configMock = $this->createMock(WebapiConfig::class); + $this->serializerMock = $this->createMock(SerializerInterface::class); + + $this->config = $objectManager->getObject( + Config::class, + [ + 'cache' => $this->webapiCacheMock, + 'webApiConfig' => $this->configMock, + 'serializer' => $this->serializerMock + ] + ); + } + + public function testGetServicesSetsTopicFromServiceContractName() + { + $services = [ + Converter::KEY_ROUTES => [ + '/V1/products' => [ + 'POST' => [ + 'service' => [ + 'class' => \Magento\Catalog\Api\ProductRepositoryInterface::class, + 'method' => 'save', + ] + ] + ] + ] + ]; + $this->configMock->expects($this->once()) + ->method('getServices') + ->willReturn($services); + + /* example of what $this->config->getServices() returns + $result = [ + 'async.V1.products.POST' => [ + 'interface' => 'Magento\Catalog\Api\ProductRepositoryInterface', + 'method' => 'save', + 'topic' => 'async.magento.catalog.api.productrepositoryinterface.save.post', + ] + ]; + */ + $result = $this->config->getServices(); + + $expectedTopic = 'async.magento.catalog.api.productrepositoryinterface.save.post'; + $lookupKey = 'async.V1.products.POST'; + $this->assertArrayHasKey($lookupKey, $result); + $this->assertEquals($result[$lookupKey]['topic'], $expectedTopic); + } +} diff --git a/app/code/Magento/WebapiAsync/etc/di.xml b/app/code/Magento/WebapiAsync/etc/di.xml index 83f1d6a78f227..7411ec0561d24 100755 --- a/app/code/Magento/WebapiAsync/etc/di.xml +++ b/app/code/Magento/WebapiAsync/etc/di.xml @@ -10,6 +10,9 @@ <type name="Magento\Webapi\Model\ServiceMetadata"> <plugin name="webapiServiceMetadataAsync" type="Magento\WebapiAsync\Plugin\ServiceMetadata" /> </type> + <type name="Magento\Webapi\Model\Cache\Type\Webapi"> + <plugin name="webapiCacheAsync" type="Magento\WebapiAsync\Plugin\Cache\Webapi" /> + </type> <virtualType name="Magento\WebapiAsync\Model\VirtualType\Rest\Config" type="Magento\Webapi\Model\Rest\Config"> <arguments> <argument name="config" xsi:type="object">Magento\WebapiAsync\Model\BulkServiceConfig</argument> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml index 2e3467fe2c7c5..3aeed3095dc45 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml @@ -5,11 +5,12 @@ * See COPYING.txt for license details. */ --> + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveProductWeeeAttributeOptionTest"> <annotations> - <features value="Weee attribute options can be removed in product page"/> + <stories value="Weee attribute options can be removed in product page"/> <title value="Weee attribute options can be removed in product page"/> <description value="Weee attribute options can be removed in product page"/> <severity value="CRITICAL"/> @@ -44,6 +45,7 @@ <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> </after> + <!-- Test Steps --> <!-- Step 1: Open created product edit page --> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget.php b/app/code/Magento/Widget/Block/Adminhtml/Widget.php index 33e6109b769db..dad318f163b4b 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget.php @@ -16,8 +16,6 @@ class Widget extends \Magento\Backend\Block\Widget\Form\Container { /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Catalog/Category/Chooser.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Catalog/Category/Chooser.php index 7e6ba87860307..230598a7e263d 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Catalog/Category/Chooser.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Catalog/Category/Chooser.php @@ -3,14 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Widget\Block\Adminhtml\Widget\Catalog\Category; /** * Category chooser for widget's layout updates - * - * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Widget\Block\Adminhtml\Widget\Catalog\Category; - class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser { /** @@ -18,7 +15,7 @@ class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser * * @param \Magento\Framework\Data\Tree\Node|array $node * @param int $level - * @return string + * @return array */ protected function _getNodeJson($node, $level = 0) { diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php index cc2af4996d562..32bae10c801c8 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php @@ -91,7 +91,7 @@ public function getMainFieldset() if ($this->_getData('main_fieldset') instanceof \Magento\Framework\Data\Form\Element\Fieldset) { return $this->_getData('main_fieldset'); } - $mainFieldsetHtmlId = 'options_fieldset' . md5($this->getWidgetType()); + $mainFieldsetHtmlId = 'options_fieldset' . hash('sha256', $this->getWidgetType()); $this->setMainFieldsetHtmlId($mainFieldsetHtmlId); $fieldset = $this->getForm()->addFieldset( $mainFieldsetHtmlId, @@ -141,7 +141,6 @@ protected function _addField($parameter) { $form = $this->getForm(); $fieldset = $this->getMainFieldset(); - //$form->getElement('options_fieldset'); // prepare element data with values (either from request of from default values) $fieldName = $parameter->getKey(); @@ -166,9 +165,13 @@ protected function _addField($parameter) if (is_array($data['value'])) { foreach ($data['value'] as &$value) { - $value = html_entity_decode($value); + if (is_string($value)) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $value = html_entity_decode($value); + } } } else { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $data['value'] = html_entity_decode($data['value']); } diff --git a/app/code/Magento/Widget/Model/Template/Filter.php b/app/code/Magento/Widget/Model/Template/Filter.php index 7c3e8e467e038..c79334f67a9c3 100644 --- a/app/code/Magento/Widget/Model/Template/Filter.php +++ b/app/code/Magento/Widget/Model/Template/Filter.php @@ -91,6 +91,10 @@ public function generateWidget($construction) $name = $params['name']; } + if (isset($this->_storeId) && !isset($params['store_id'])) { + $params['store_id'] = $this->_storeId; + } + // validate required parameter type or id if (!empty($params['type'])) { $type = $params['type']; diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 5ba03d008ded0..d07e84186b2c9 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -151,8 +151,8 @@ public function getConfigAsObject($type) $widget = $this->getAsCanonicalArray($widget); // Save all nodes to object data - $object->setType($type); $object->setData($widget); + $object->setType($type); // Correct widget parameters and convert its data to objects $newParams = $this->prepareWidgetParameters($object); @@ -248,17 +248,13 @@ public function getWidgets($filters = []) $result = $widgets; // filter widgets by params - if (is_array($filters) && count($filters) > 0) { + if (is_array($filters) && !empty($filters)) { foreach ($widgets as $code => $widget) { - try { - foreach ($filters as $field => $value) { - if (!isset($widget[$field]) || (string)$widget[$field] != $value) { - throw new \Exception(); - } + foreach ($filters as $field => $value) { + if (!isset($widget[$field]) || (string)$widget[$field] != $value) { + unset($result[$code]); + break; } - } catch (\Exception $e) { - unset($result[$code]); - continue; } } } @@ -323,8 +319,6 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) $directive .= $this->getWidgetPageVarName($params); - $directive .= sprintf(' type_name="%s"', $widget['name']); - $directive .= '}}'; if ($asIs) { diff --git a/app/code/Magento/Widget/Model/Widget/Config.php b/app/code/Magento/Widget/Model/Widget/Config.php index 4f81ef33f47f7..00b055b35a69d 100644 --- a/app/code/Magento/Widget/Model/Widget/Config.php +++ b/app/code/Magento/Widget/Model/Widget/Config.php @@ -120,6 +120,7 @@ public function getWidgetPlaceholderImageUrls() /** * Return url to error image + * * @return string */ public function getErrorImageUrl() @@ -129,6 +130,7 @@ public function getErrorImageUrl() /** * Return url to wysiwyg plugin + * * @return string */ public function getWysiwygJsPluginSrc() @@ -157,7 +159,7 @@ public function getWidgetWindowUrl($config) } } - if (count($skipped) > 0) { + if (!empty($skipped)) { $params['skip_widgets'] = $this->encodeWidgetsToQuery($skipped); } return $this->_backendUrl->getUrl('adminhtml/widget/index', $params); @@ -189,6 +191,8 @@ public function decodeWidgetsFromQuery($queryParam) } /** + * Get available widgets. + * * @param \Magento\Framework\DataObject $config Editor element config * @return array */ @@ -202,7 +206,7 @@ public function getAvailableWidgets($config) if (is_array($skipped) && in_array($widget['type'], $skipped)) { continue; } - $result[] = $widget['name']->getText(); + $result[$widget['type']] = $widget['name']->getText(); } } diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index 642ad6a268201..969ab58b04876 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -48,7 +48,7 @@ <click selector="{{AdminNewWidgetSection.saveAndContinue}}" stepKey="clickSaveWidget"/> <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> </actionGroup> - + <actionGroup name="AdminDeleteWidgetActionGroup"> <arguments> <argument name="widget"/> @@ -65,4 +65,17 @@ <waitForPageLoad stepKey="waitForDeleteLoad"/> <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been deleted" stepKey="seeSuccess"/> </actionGroup> + <actionGroup name="AdminCreateProductLinkWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <selectOption selector="{{AdminNewWidgetSection.selectTemplate}}" userInput="{{widget.template}}" after="waitForPageLoad" stepKey="setTemplate"/> + <waitForAjaxLoad after="setTemplate" stepKey="waitForPageLoad2"/> + <click selector="{{AdminNewWidgetSection.selectProduct}}" after="clickWidgetOptions" stepKey="clickSelectProduct"/> + <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" after="clickSelectProduct" stepKey="fillProductNameInFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" after="fillProductNameInFilter" stepKey="applyFilter"/> + <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" after="applyFilter" stepKey="selectProduct"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetActionGroup.xml new file mode 100644 index 0000000000000..c303b7cc8e900 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetActionGroup.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateWidgetWithBlockActionGroup"> + <arguments> + <argument name="widget"/> + <argument name="block" type="string"/> + </arguments> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="createWidgetPage"/> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widget.type}}" stepKey="selectWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widget.designTheme}}" stepKey="selectWidgetDesignTheme"/> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="continue"/> + <waitForElement selector="{{AdminNewWidgetSection.widgetTitle}}" time="30" stepKey="waitForElement"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillWidgetTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_id}}" stepKey="selectWidgetStoreView"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + <waitForPageLoad stepKey="waitForLoad1"/> + <scrollTo selector="{{AdminNewWidgetSection.selectDisplayOn}}" stepKey="scrollToElement" /> + <selectOption selector="{{AdminNewWidgetSection.selectDisplayOn}}" userInput="{{widget.display}}" stepKey="selectWidgetDisplayOn"/> + <waitForElement selector="{{AdminNewWidgetSection.selectContainer}}" time="30" stepKey="waitForContainer"/> + <selectOption selector="{{AdminNewWidgetSection.selectContainer}}" userInput="{{widget.container}}" stepKey="selectWidgetContainer"/> + <scrollToTopOfPage stepKey="scrollToAddresses"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad1"/> + <click selector="{{AdminNewWidgetSection.widgetOptions}}" stepKey="goToWidgetOptions"/> + <waitForElement selector="{{AdminNewWidgetSection.widgetSelectBlock}}" time="60" stepKey="waitForSelectBlock"/> + <click selector="{{AdminNewWidgetSection.widgetSelectBlock}}" stepKey="openSelectBlock"/> + <waitForPageLoad stepKey="waitForLoadBlocks"/> + <selectOption selector="{{AdminNewWidgetSection.blockStatus}}" userInput="Disable" stepKey="chooseStatus"/> + <fillField selector="{{AdminNewWidgetSection.selectBlockTitle}}" userInput="{{block}}" stepKey="fillBlockTitle"/> + <click selector="{{AdminNewWidgetSection.searchBlock}}" stepKey="searchBlock"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminNewWidgetSection.searchedBlock}}" stepKey="clickSearchedBlock"/> + <waitForPageLoad stepKey="wait"/> + <click selector="{{AdminNewWidgetSection.saveWidget}}" stepKey="saveWidget"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see userInput="The widget instance has been saved." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Widget/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..8fa652b8f73eb --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Data/AdminMenuData.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="AdminMenuContentElementsWidgets"> + <data key="pageTitle">Widgets</data> + <data key="title">Widgets</data> + <data key="dataUiId">magento-widget-cms-widget-instance</data> + </entity> +</entities> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetData.xml new file mode 100644 index 0000000000000..4c6e98aafd765 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetData.xml @@ -0,0 +1,19 @@ +<?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="WidgetWithBlock" type="widget"> + <data key="type">CMS Static Block</data> + <data key="designTheme">Magento Luma</data> + <data key="name" unique="suffix">testName</data> + <data key="store_id">All Store Views</data> + <data key="display">All Pages</data> + <data key="container">Page Top</data> + </entity> +</entities> diff --git a/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.xml b/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.xml index 421899ad21646..46209f9e5f015 100644 --- a/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.xml +++ b/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.xml @@ -7,7 +7,7 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminWidgetsPage" url="admin/widget_instance/" area="admin" module="Magento_Widget"> <section name="AdminWidgetsSection"/> </page> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index aea74b573ce11..eebd6c10b5085 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -17,6 +17,7 @@ <element name="addLayoutUpdate" type="button" selector=".action-default.scalable.action-add"/> <element name="selectDisplayOn" type="select" selector="#widget_instance[0][page_group]"/> <element name="selectContainer" type="select" selector="#all_pages_0>table>tbody>tr>td:nth-child(1)>div>div>select"/> + <element name="selectTemplate" type="select" selector=".widget-layout-updates .block_template_container .select"/> <element name="widgetOptions" type="select" selector="#widget_instace_tabs_properties_section"/> <element name="addNewCondition" type="select" selector=".rule-param.rule-param-new-child"/> <element name="selectCondition" type="input" selector="#conditions__1__new_child"/> @@ -25,6 +26,12 @@ <element name="applyParameter" type="button" selector=".rule-param-apply"/> <element name="openChooser" type="button" selector=".rule-chooser-trigger"/> <element name="selectAll" type="checkbox" selector=".admin__control-checkbox"/> + <element name="widgetSelectBlock" type="button" selector="//button[@class='action-default scalable btn-chooser']"/> + <element name="selectBlockTitle" type="input" selector="//input[@name='chooser_title']"/> + <element name="searchBlock" type="button" selector="//div[@class='admin__filter-actions']/button[@title='Search']"/> + <element name="blockStatus" type="select" selector="//select[@name='chooser_is_active']"/> + <element name="searchedBlock" type="button" selector="//*[@class='magento-message']//tbody/tr/td[1]"/> + <element name="saveWidget" type="select" selector="#save"/> <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.xml index 5a0515d35ad58..f3282362d9aa1 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminWidgetsSection"> <element name="widgetTitleSearch" type="input" selector="#widgetInstanceGrid_filter_title"/> <element name="searchButton" type="button" selector=".action-default.scalable.action-secondary"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml index 23908626389f9..0e2f6cec73a92 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontWidgetsSection"> <element name="widgetProductsGrid" type="block" selector=".block.widget.block-products-list.grid"/> <element name="widgetProductName" type="text" selector=".product-item-name"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml new file mode 100644 index 0000000000000..f5af024ec1d51 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?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="AdminContentWidgetsNavigateMenuTest"> + <annotations> + <features value="Widget"/> + <stories value="Menu Navigation"/> + <title value="Admin content widgets navigate menu test"/> + <description value="Admin should be able to navigate to Content > Widgets"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14147"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentWidgetsPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsWidgets.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentElementsWidgets.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php b/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php index 5c546d7e2435c..850a3fbe83211 100644 --- a/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php +++ b/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php @@ -224,7 +224,6 @@ public function testGetWidgetDeclaration() $this->assertContains('title="my "widget""', $result); $this->assertContains('conditions_encoded="encoded-conditions-string"', $result); $this->assertContains('page_var_name="pasdf"', $result); - $this->assertContains('type_name=""}}', $result); } /** @@ -275,7 +274,6 @@ public function testGetWidgetDeclarationWithZeroValueParam() ); $this->assertContains('{{widget type="Magento\CatalogWidget\Block\Product\ProductsList"', $result); $this->assertContains('page_var_name="pasdf"', $result); - $this->assertContains('type_name=""}}', $result); $this->assertContains('products_count=""', $result); } } diff --git a/app/code/Magento/Wishlist/Block/AbstractBlock.php b/app/code/Magento/Wishlist/Block/AbstractBlock.php index bb8138fb87a3e..981a0da1d241f 100644 --- a/app/code/Magento/Wishlist/Block/AbstractBlock.php +++ b/app/code/Magento/Wishlist/Block/AbstractBlock.php @@ -228,7 +228,7 @@ public function hasDescription($item) } /** - * Retrieve formated Date + * Retrieve formatted Date * * @param string $date * @deprecated diff --git a/app/code/Magento/Wishlist/Block/Cart/Item/Renderer/Actions/MoveToWishlist.php b/app/code/Magento/Wishlist/Block/Cart/Item/Renderer/Actions/MoveToWishlist.php index 823849ed41047..eba1f7da72742 100644 --- a/app/code/Magento/Wishlist/Block/Cart/Item/Renderer/Actions/MoveToWishlist.php +++ b/app/code/Magento/Wishlist/Block/Cart/Item/Renderer/Actions/MoveToWishlist.php @@ -10,6 +10,8 @@ use Magento\Wishlist\Helper\Data; /** + * Class MoveToWishlist + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/Wishlist/Block/Customer/Sharing.php b/app/code/Magento/Wishlist/Block/Customer/Sharing.php index 6fbf5a23dca22..40fd00d6143a5 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Sharing.php +++ b/app/code/Magento/Wishlist/Block/Customer/Sharing.php @@ -11,9 +11,14 @@ */ namespace Magento\Wishlist\Block\Customer; +use Magento\Captcha\Block\Captcha; + /** + * Class Sharing + * * @api * @since 100.0.2 + * @package Magento\Wishlist\Block\Customer */ class Sharing extends \Magento\Framework\View\Element\Template { @@ -60,6 +65,20 @@ public function __construct( */ protected function _prepareLayout() { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'share_wishlist_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + $this->pageConfig->getTitle()->set(__('Wish List Sharing')); } diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php index 2b1b6d44b425c..d02f2229401c1 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php @@ -5,13 +5,13 @@ */ /** - * Wishlist block customer items - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer; /** + * Wishlist block customer items. + * * @api * @since 100.0.2 */ @@ -29,6 +29,11 @@ class Wishlist extends \Magento\Wishlist\Block\AbstractBlock */ protected $_helperPool; + /** + * @var \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + protected $_collection; + /** * @var \Magento\Customer\Helper\Session\CurrentCustomer */ @@ -78,14 +83,64 @@ protected function _prepareCollection($collection) } /** - * Preparing global layout + * Paginate Wishlist Product Items collection * * @return void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + */ + private function paginateCollection() + { + $page = $this->getRequest()->getParam("p", 1); + $limit = $this->getRequest()->getParam("limit", 10); + $this->_collection + ->setPageSize($limit) + ->setCurPage($page); + } + + /** + * Retrieve Wishlist Product Items collection + * + * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + public function getWishlistItems() + { + if ($this->_collection === null) { + $this->_collection = $this->_createWishlistItemCollection(); + $this->_prepareCollection($this->_collection); + $this->paginateCollection(); + } + return $this->_collection; + } + + /** + * Preparing global layout + * + * @return $this */ protected function _prepareLayout() { parent::_prepareLayout(); $this->pageConfig->getTitle()->set(__('My Wish List')); + $this->getChildBlock('wishlist_item_pager') + ->setUseContainer( + true + )->setShowAmounts( + true + )->setFrameLength( + $this->_scopeConfig->getValue( + 'design/pagination/pagination_frame', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + )->setJump( + $this->_scopeConfig->getValue( + 'design/pagination/pagination_frame_skip', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + )->setLimit( + $this->getLimit() + ) + ->setCollection($this->getWishlistItems()); + return $this; } /** @@ -198,6 +253,7 @@ public function getAddToCartQty(\Magento\Wishlist\Model\Item $item) /** * Get add all to cart params for POST request + * * @return string */ public function getAddAllToCartParams() @@ -209,7 +265,7 @@ public function getAddAllToCartParams() } /** - * @return string + * @inheritdoc */ protected function _toHtml() { diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php index cafb6a5291481..40882ae00dae1 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php @@ -5,14 +5,15 @@ */ /** - * Wishlist for item column in customer wishlist - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Model for item column in customer wishlist. + * * @api + * @deprecated * @since 100.0.2 */ class Actions extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php index fe0683a52fe97..b043a8d4b684c 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php @@ -6,8 +6,6 @@ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; -use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; - /** * Wishlist block customer item cart column * @@ -37,28 +35,4 @@ public function getProductItem() { return $this->getItem()->getProduct(); } - - /** - * Get min and max qty for wishlist form. - * - * @return array - */ - public function getMinMaxQty() - { - $stockItem = $this->stockRegistry->getStockItem( - $this->getItem()->getProduct()->getId(), - $this->getItem()->getProduct()->getStore()->getWebsiteId() - ); - - $params = []; - - $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); - if ($stockItem->getMaxSaleQty()) { - $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); - } else { - $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; - } - - return $params; - } } diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php index 2d75956858a0a..53f67626e956d 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Comment extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php index 53ca78c63524d..c4c786961694b 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php @@ -5,14 +5,15 @@ */ /** - * Edit item in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Edit item in customer wishlist table. + * * @api + * @deprecated * @since 100.0.2 */ class Edit extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php index 33fb0f7325cdd..b7eaf53fc23b5 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Info extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php index 57703b9300db8..09f5014edead6 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php @@ -5,14 +5,15 @@ */ /** - * Delete item column in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Delete item column in customer wishlist table + * * @api + * @deprecated * @since 100.0.2 */ class Remove extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Controller/Index/Allcart.php b/app/code/Magento/Wishlist/Controller/Index/Allcart.php index 8463a00c866c5..958e0f49ca1b6 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Allcart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Allcart.php @@ -5,14 +5,17 @@ */ namespace Magento\Wishlist\Controller\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Data\Form\FormKey\Validator; -use Magento\Framework\App\Action; use Magento\Framework\App\Action\Context; use Magento\Wishlist\Controller\WishlistProviderInterface; use Magento\Wishlist\Model\ItemCarrier; use Magento\Framework\Controller\ResultFactory; -class Allcart extends \Magento\Wishlist\Controller\AbstractIndex +/** + * Action Add All to Cart + */ +class Allcart extends \Magento\Wishlist\Controller\AbstractIndex implements HttpPostActionInterface { /** * @var WishlistProviderInterface diff --git a/app/code/Magento/Wishlist/Controller/Index/Cart.php b/app/code/Magento/Wishlist/Controller/Index/Cart.php index 0e826d83a52f6..da37609d688e7 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Cart.php @@ -3,16 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Wishlist\Controller\Index; -use Magento\Framework\App\Action; use Magento\Catalog\Model\Product\Exception as ProductException; +use Magento\Framework\App\Action; use Magento\Framework\Controller\ResultFactory; /** + * Add wishlist item to shopping cart and remove from wishlist controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Cart extends \Magento\Wishlist\Controller\AbstractIndex +class Cart extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var \Magento\Wishlist\Controller\WishlistProviderInterface @@ -195,12 +198,12 @@ public function execute() } } } catch (ProductException $e) { - $this->messageManager->addError(__('This product(s) is out of stock.')); + $this->messageManager->addErrorMessage(__('This product(s) is out of stock.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addNotice($e->getMessage()); + $this->messageManager->addNoticeMessage($e->getMessage()); $redirectUrl = $configureUrl; } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t add the item to the cart right now.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t add the item to the cart right now.')); } $this->helper->calculate(); diff --git a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php index b87afa8e5d0c4..742b2a91e9317 100644 --- a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php +++ b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Wishlist\Controller\Index; use Magento\Framework\App\Action; @@ -51,7 +53,6 @@ public function __construct( * * @return \Magento\Framework\Controller\Result\Forward * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExitExpression) */ public function execute() { diff --git a/app/code/Magento/Wishlist/Controller/Index/Fromcart.php b/app/code/Magento/Wishlist/Controller/Index/Fromcart.php index 49396004427f2..52d0951f1670c 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Fromcart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Fromcart.php @@ -8,7 +8,6 @@ use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart as CheckoutCart; -use Magento\Customer\Model\Session; use Magento\Framework\App\Action; use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Escaper; @@ -19,9 +18,11 @@ use Magento\Wishlist\Helper\Data as WishlistHelper; /** + * Add cart item to wishlist and remove from cart controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Fromcart extends \Magento\Wishlist\Controller\AbstractIndex +class Fromcart extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var WishlistProviderInterface diff --git a/app/code/Magento/Wishlist/Controller/Index/Send.php b/app/code/Magento/Wishlist/Controller/Index/Send.php index c2389af6a2282..a4e8258b9d67e 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Send.php +++ b/app/code/Magento/Wishlist/Controller/Index/Send.php @@ -8,16 +8,28 @@ use Magento\Framework\App\Action; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Session\Generic as WishlistSession; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\View\Result\Layout as ResultLayout; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Framework\Exception\LocalizedException; +use Magento\Customer\Model\Customer; /** + * Class Send + * + * @package Magento\Wishlist\Controller\Index * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Send extends \Magento\Wishlist\Controller\AbstractIndex +class Send extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var \Magento\Customer\Helper\View @@ -69,6 +81,16 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex */ protected $storeManager; + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + /** * @param Action\Context $context * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator @@ -81,6 +103,8 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex * @param WishlistSession $wishlistSession * @param ScopeConfigInterface $scopeConfig * @param StoreManagerInterface $storeManager + * @param CaptchaHelper|null $captchaHelper + * @param CaptchaStringResolver|null $captchaStringResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -94,7 +118,9 @@ public function __construct( \Magento\Customer\Helper\View $customerHelperView, WishlistSession $wishlistSession, ScopeConfigInterface $scopeConfig, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?CaptchaHelper $captchaHelper = null, + ?CaptchaStringResolver $captchaStringResolver = null ) { $this->_formKeyValidator = $formKeyValidator; $this->_customerSession = $customerSession; @@ -106,6 +132,10 @@ public function __construct( $this->wishlistSession = $wishlistSession; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; + $this->captchaHelper = $captchaHelper ?: ObjectManager::getInstance()->get(CaptchaHelper::class); + $this->captchaStringResolver = $captchaStringResolver ? + : ObjectManager::getInstance()->get(CaptchaStringResolver::class); + parent::__construct($context); } @@ -114,6 +144,7 @@ public function __construct( * * @return \Magento\Framework\Controller\Result\Redirect * @throws NotFoundException + * @throws \Zend_Validate_Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -122,11 +153,25 @@ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $captchaForName = 'share_wishlist_form'; + /** @var CaptchaModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaForName); + if (!$this->_formKeyValidator->validate($this->getRequest())) { $resultRedirect->setPath('*/*/'); return $resultRedirect; } + $isCorrectCaptcha = $this->validateCaptcha($captchaModel, $captchaForName); + + $this->logCaptchaAttempt($captchaModel); + + if (!$isCorrectCaptcha) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $resultRedirect->setPath('*/*/share'); + return $resultRedirect; + } + $wishlist = $this->wishlistProvider->getWishlist(); if (!$wishlist) { throw new NotFoundException(__('Page not found.')); @@ -288,4 +333,46 @@ protected function getWishlistItems(ResultLayout $resultLayout) ->getBlock('wishlist.email.items') ->toHtml(); } + + /** + * Log customer action attempts + * + * @param CaptchaModel $captchaModel + * @return void + */ + private function logCaptchaAttempt(CaptchaModel $captchaModel): void + { + /** @var Customer $customer */ + $customer = $this->_customerSession->getCustomer(); + $email = ''; + + if ($customer->getId()) { + $email = $customer->getEmail(); + } + + $captchaModel->logAttempt($email); + } + + /** + * Captcha validate logic + * + * @param CaptchaModel $captchaModel + * @param string $captchaFormName + * @return bool + */ + private function validateCaptcha(CaptchaModel $captchaModel, string $captchaFormName) : bool + { + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $this->getRequest(), + $captchaFormName + ); + + if (!$captchaModel->isCorrect($word)) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/Wishlist/Controller/Index/Update.php b/app/code/Magento/Wishlist/Controller/Index/Update.php index 056d58b4c70be..b56aa4b5b3c8d 100755 --- a/app/code/Magento/Wishlist/Controller/Index/Update.php +++ b/app/code/Magento/Wishlist/Controller/Index/Update.php @@ -6,10 +6,14 @@ namespace Magento\Wishlist\Controller\Index; use Magento\Framework\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Controller\ResultFactory; -class Update extends \Magento\Wishlist\Controller\AbstractIndex +/** + * Class Update + */ +class Update extends \Magento\Wishlist\Controller\AbstractIndex implements HttpPostActionInterface { /** * @var \Magento\Wishlist\Controller\WishlistProviderInterface @@ -83,8 +87,6 @@ public function execute() )->defaultCommentString() ) { $description = ''; - } elseif (!strlen($description)) { - $description = $item->getDescription(); } $qty = null; diff --git a/app/code/Magento/Wishlist/Helper/Data.php b/app/code/Magento/Wishlist/Helper/Data.php index 3b9f431566da0..3d25e16294fcd 100644 --- a/app/code/Magento/Wishlist/Helper/Data.php +++ b/app/code/Magento/Wishlist/Helper/Data.php @@ -13,6 +13,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * * @api * @since 100.0.2 @@ -171,7 +172,7 @@ public function setCustomer(\Magento\Customer\Api\Data\CustomerInterface $custom public function getCustomer() { if (!$this->_currentCustomer && $this->_customerSession->isLoggedIn()) { - $this->_currentCustomer = $this->_customerSession->getCustomerDataObject(); + $this->_currentCustomer = $this->_customerSession->getCustomerData(); } return $this->_currentCustomer; } @@ -355,7 +356,7 @@ public function getMoveFromCartParams($itemId) * * @param \Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item * - * @return string|false + * @return string|false */ public function getUpdateParams($item) { @@ -382,7 +383,7 @@ public function getUpdateParams($item) * Retrieve params for adding item to shopping cart * * @param string|\Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item - * @return string + * @return string */ public function getAddToCartUrl($item) { @@ -428,7 +429,7 @@ public function addRefererToParams(array $params) * Retrieve URL for adding item to shopping cart from shared wishlist * * @param string|\Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item - * @return string + * @return string */ public function getSharedAddToCartUrl($item) { diff --git a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php index 9ccbf80f99a0c..ff59e0cdd7b91 100644 --- a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php @@ -118,10 +118,8 @@ public function __construct( */ public function isAllowed() { - return $this->scopeConfig->isSetFlag( - 'rss/wishlist/active', - ScopeInterface::SCOPE_STORE - ); + return $this->scopeConfig->isSetFlag('rss/wishlist/active', ScopeInterface::SCOPE_STORE) + && $this->getWishlist()->getCustomerId() === $this->wishlistHelper->getCustomer()->getId(); } /** @@ -185,8 +183,8 @@ public function getRssData() } } else { $data = [ - 'title' => __('We cannot retrieve the Wish List.'), - 'description' => __('We cannot retrieve the Wish List.'), + 'title' => __('We cannot retrieve the Wish List.')->render(), + 'description' => __('We cannot retrieve the Wish List.')->render(), 'link' => $this->urlBuilder->getUrl(), 'charset' => 'UTF-8', ]; @@ -202,7 +200,7 @@ public function getRssData() */ public function getCacheKey() { - return 'rss_wishlist_data'; + return 'rss_wishlist_data_' . $this->getWishlist()->getId(); } /** @@ -224,7 +222,7 @@ public function getHeader() { $customerId = $this->getWishlist()->getCustomerId(); $customer = $this->customerFactory->create()->load($customerId); - $title = __('%1\'s Wishlist', $customer->getName()); + $title = __('%1\'s Wishlist', $customer->getName())->render(); $newUrl = $this->urlBuilder->getUrl( 'wishlist/shared/index', ['code' => $this->getWishlist()->getSharingCode()] diff --git a/app/code/Magento/Wishlist/Setup/Patch/Data/ConvertSerializedData.php b/app/code/Magento/Wishlist/Setup/Patch/Data/ConvertSerializedData.php index 76f27756d8270..0e809c4703921 100644 --- a/app/code/Magento/Wishlist/Setup/Patch/Data/ConvertSerializedData.php +++ b/app/code/Magento/Wishlist/Setup/Patch/Data/ConvertSerializedData.php @@ -3,20 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Wishlist\Setup\Patch\Data; -use Magento\Framework\DB\FieldDataConverterFactory; use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\DB\Select\QueryModifierFactory; +use Magento\Framework\DB\FieldDataConverterFactory; use Magento\Framework\DB\Query\Generator as QueryGenerator; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select\QueryModifierFactory; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class ConvertSerializedData - * @package Magento\Wishlist\Setup\Patch + * Convert serialized wishlist item data. */ class ConvertSerializedData implements DataPatchInterface, PatchVersionInterface { @@ -60,7 +57,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -68,7 +65,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -76,7 +73,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -84,13 +81,19 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { return []; } - + + /** + * Convert serialized whishlist item data. + * + * @throws \Magento\Framework\DB\FieldDataConversionException + * @throws \Magento\Framework\Exception\LocalizedException + */ private function convertSerializedData() { $connection = $this->moduleDataSetup->getConnection(); diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml index 7bb42e12d1451..a1c5b9eae5c49 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml @@ -28,7 +28,7 @@ <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productAddToWishlist}}" stepKey="WaitForWishList"/> <click selector="{{StorefrontProductInfoMainSection.productAddToWishlist}}" stepKey="addProductToWishlistClickAddToWishlist" /> <waitForElement selector="{{StorefrontCustomerWishlistSection.successMsg}}" time="30" stepKey="addProductToWishlistWaitForSuccessMessage"/> - <see selector="{{StorefrontCustomerWishlistSection.successMsg}}" userInput="{{productVar.name}} has been added to your Wish List." stepKey="addProductToWishlistSeeProductNameAddedToWishlist"/> + <see selector="{{StorefrontCustomerWishlistSection.successMsg}}" userInput="{{productVar.name}} has been added to your Wish List. Click here to continue shopping." stepKey="addProductToWishlistSeeProductNameAddedToWishlist"/> <seeCurrentUrlMatches regex="~/wishlist_id/\d+/$~" stepKey="seeCurrentUrlMatches"/> </actionGroup> @@ -88,4 +88,13 @@ <click selector="{{StorefrontCustomerWishlistProductSection.ProductUpdateWishList}}" stepKey="submitUpdateWishlist"/> <see selector="{{StorefrontCustomerWishlistProductSection.ProductSuccessUpdateMessage}}" userInput="{{product.name}} has been updated in your Wish List." stepKey="successMessage"/> </actionGroup> + + <!-- Share wishlist --> + <actionGroup name="StorefrontCustomerShareWishlistActionGroup"> + <click selector="{{StorefrontCustomerWishlistProductSection.productShareWishList}}" stepKey="clickMyWishListButton"/> + <fillField userInput="{{Wishlist.shareInfo_emails}}" selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistEmail}}" stepKey="fillEmailsForShare"/> + <fillField userInput="{{Wishlist.shareInfo_message}}" selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistTextMessage}}" stepKey="fillShareMessage"/> + <click selector="{{StorefrontCustomerWishlistShareSection.ProductShareWishlistButton}}" stepKey="sendWishlist"/> + <see selector="{{StorefrontCustomerWishlistProductSection.productSuccessShareMessage}}" userInput="Your wish list has been shared." stepKey="successMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml index 811871bf685ae..c6a9704698b05 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Data/WishlistData.xml @@ -12,5 +12,7 @@ <var key="product" entityType="product" entityKey="id"/> <var key="customer_email" entityType="customer" entityKey="email"/> <var key="customer_password" entityType="customer" entityKey="password"/> + <data key="shareInfo_emails" entityType="customer" >JohnDoe123456789@example.com,JohnDoe987654321@example.com,JohnDoe123456abc@example.com</data> + <data key="shareInfo_message" entityType="customer">Sharing message.</data> </entity> </entities> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Page/StorefrontCustomerWishlistSharePage.xml b/app/code/Magento/Wishlist/Test/Mftf/Page/StorefrontCustomerWishlistSharePage.xml new file mode 100644 index 0000000000000..6d6151648c5ee --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Page/StorefrontCustomerWishlistSharePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerWishlistSharePage" url="/wishlist/index/share/wishlist_id/{{wishlistId}}/" area="storefront" module="Magento_Wishlist"> + <section name="StorefrontCustomerWishlistShareSection"/> + </page> +</pages> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml index 7a767e42e82bf..ef619726b76ab 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml @@ -19,6 +19,8 @@ <element name="ProductQuantity" type="input" selector="//a[contains(text(), '{{productName}}')]/ancestor::div[@class='product-item-info']//input[@class='input-text qty']" parameterized="true"/> <element name="ProductUpdateWishList" type="button" selector=".column.main .actions-toolbar .action.update" timeout="30"/> <element name="ProductAddAllToCart" type="button" selector=".column.main .actions-toolbar .action.tocart" timeout="30"/> + <element name="productShareWishList" type="button" selector="button.action.share" timeout="30" /> <element name="ProductSuccessUpdateMessage" type="text" selector="//div[1]/div[2]/div/div/div"/> + <element name="productSuccessShareMessage" type="text" selector="div.message-success"/> </section> </sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml new file mode 100644 index 0000000000000..76b99ba56a327 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerWishlistShareSection"> + <element name="ProductShareWishlistEmail" type="input" selector="#email_address"/> + <element name="ProductShareWishlistTextMessage" type="input" selector="#message"/> + <element name="ProductShareWishlistButton" type="button" selector=".action.submit.primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml new file mode 100644 index 0000000000000..4e6a062c7993d --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml @@ -0,0 +1,72 @@ +<?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="ConfProdAddToCartWishListWithUnselectedAttrTest"> + <annotations> + <stories value="Wishlist"/> + <group value="wishlist"/> + <title value="Adding configurable product to Cart from Wish List with unselected attributes"/> + <description value="Verify adding configurable product to Cart from Wish List when attributes is unselected"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-95897"/> + <useCaseId value="MAGETWO-95837"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable product--> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete the first simple product --> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForLogin"/> + + <!--Go To Created Product Page--> + <amOnPage stepKey="goToCreatedProductPage" url="{{_defaultProduct.urlKey}}.html"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="checkDropDownProductOption"/> + <selectOption userInput="{{colorProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <selectOption userInput="{{colorProductAttribute2.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption2"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="clickDropDownProductOption"/> + + <!--Click Add to Wish List link--> + <click selector="{{StorefrontProductPageSection.addToWishlist}}" stepKey="addFirstPnroductToWishlist"/> + + <waitForPageLoad stepKey="waitForLoading"/> + + <!--Click "Add All to Cart" button--> + <click selector="{{StorefrontCustomerWishlistProductSection.ProductAddAllToCart}}" stepKey="addAllToCart"/> + <waitForElementVisible stepKey="waitForErrorAppears" selector="{{StorefrontMessagesSection.error}}"/> + + <!--Assert Correct Error Message--> + <see userInput="You need to choose options for your item for" stepKey="assertCorrectErrorMessage"/> + <dontSee userInput="1 product(s) have been added to shopping cart" stepKey="dontSeeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml index 42d4203999a44..6b951c89208c2 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -6,7 +6,8 @@ */ --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="ConfigurableProductChildImageShouldBeShownOnWishListTest"> <annotations> <features value="Wishlist"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index b91f796e6a18f..ede63322235f2 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -16,6 +16,9 @@ <group value="wishlist"/> <severity value="AVERAGE"/> <testCaseId value="MAGETWO-95678"/> + <skip> + <issueId value="MC-13867"/> + </skip> </annotations> <before> <createData entity="customStoreGroup" stepKey="storeGroup"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml new file mode 100644 index 0000000000000..87c5ed950949f --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.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="StorefrontShareWishlistEntityTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Customer wishlist"/> + <title value="Customer should be able to share a persistent wishlist"/> + <description value="Customer should be able to share a persistent wishlist"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <testCaseId value="MC-13976"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + + <!-- Sign in as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$$category$$"/> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerShareWishlistActionGroup" stepKey="shareWishlist"/> + + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index 9f11de49adcd4..e482449f623fc 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -1,59 +1,58 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="StorefrontUpdateWishlistTest"> - <annotations> - <title value="Displaying of message after Wish List update"/> - <stories value="MAGETWO-91666: Wishlist update does not return a success message"/> - <description value="Displaying of message after Wish List update"/> - <features value="Wishlist"/> - <severity value="MAJOR"/> - <testCaseId value="MAGETWO-94296"/> - <group value="Wishlist"/> - </annotations> - - <before> - <createData entity="SimpleSubCategory" stepKey="category"/> - <createData entity="SimpleProduct" stepKey="product"> - <requiredEntity createDataKey="category"/> - </createData> - <createData entity="Simple_US_Customer" stepKey="customer"/> - </before> - - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> - <argument name="Customer" value="$$customer$$"/> - </actionGroup> - - <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> - <argument name="category" value="$$category$$"/> - <argument name="product" value="$$product$$"/> - </actionGroup> - - <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> - <argument name="productVar" value="$$product$$"/> - </actionGroup> - - <actionGroup ref="StorefrontCustomerCheckProductInWishlist" stepKey="checkProductInWishlist"> - <argument name="productVar" value="$$product$$"/> - </actionGroup> - - <actionGroup ref="StorefrontCustomerEditProductInWishlist" stepKey="updateProductInWishlist"> - <argument name="product" value="$$product$$"/> - <argument name="description" value="some text"/> - <argument name="quantity" value="2"/> - </actionGroup> - - <after> - <deleteData createDataKey="category" stepKey="deleteCategory"/> - <deleteData createDataKey="product" stepKey="deleteProduct"/> - <deleteData createDataKey="customer" stepKey="deleteCustomer"/> - </after> - - </test> -</tests> \ No newline at end of file +<?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="StorefrontUpdateWishlistTest"> + <annotations> + <title value="Displaying of message after Wish List update"/> + <stories value="MAGETWO-91666: Wishlist update does not return a success message"/> + <description value="Displaying of message after Wish List update"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94296"/> + <group value="Wishlist"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + </before> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$$category$$"/> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerCheckProductInWishlist" stepKey="checkProductInWishlist"> + <argument name="productVar" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerEditProductInWishlist" stepKey="updateProductInWishlist"> + <argument name="product" value="$$product$$"/> + <argument name="description" value="some text"/> + <argument name="quantity" value="2"/> + </actionGroup> + + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php index d89f6e43e07be..e9061f1f3d5f8 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php @@ -735,7 +735,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->willThrowException(new ProductException(__('Test Phrase'))); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('This product(s) is out of stock.', null) ->willReturnSelf(); @@ -901,7 +901,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('message'))); $this->messageManagerMock->expects($this->once()) - ->method('addNotice') + ->method('addNoticeMessage') ->with('message', null) ->willReturnSelf(); @@ -1073,7 +1073,7 @@ public function testExecuteWithEditQuantity() ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('message'))); $this->messageManagerMock->expects($this->once()) - ->method('addNotice') + ->method('addNoticeMessage') ->with('message', null) ->willReturnSelf(); diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php index a8c0fbb951cce..47148f7878134 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php @@ -5,32 +5,24 @@ */ namespace Magento\Wishlist\Test\Unit\Controller\Index; -use Magento\Customer\Helper\View as CustomerViewHelper; use Magento\Customer\Model\Data\Customer as CustomerData; -use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\Action\Context as ActionContext; -use Magento\Framework\App\Area; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Event\ManagerInterface as EventManagerInterface; -use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Session\Generic as WishlistSession; -use Magento\Framework\Translate\Inline\StateInterface as TranslateInlineStateInterface; use Magento\Framework\UrlInterface; -use Magento\Framework\View\Layout; use Magento\Framework\View\Result\Layout as ResultLayout; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; use Magento\Wishlist\Controller\Index\Send; use Magento\Wishlist\Controller\WishlistProviderInterface; -use Magento\Wishlist\Model\Config as WishlistConfig; -use Magento\Wishlist\Model\Wishlist; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Customer\Model\Session; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -47,36 +39,12 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var FormKeyValidator |\PHPUnit_Framework_MockObject_MockObject */ protected $formKeyValidator; - /** @var CustomerSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerSession; - /** @var WishlistProviderInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $wishlistProvider; - /** @var WishlistConfig |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistConfig; - - /** @var TransportBuilder |\PHPUnit_Framework_MockObject_MockObject */ - protected $transportBuilder; - - /** @var TranslateInlineStateInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $inlineTranslation; - - /** @var CustomerViewHelper |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerViewHelper; - - /** @var WishlistSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistSession; - - /** @var ScopeConfigInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfig; - /** @var Store |\PHPUnit_Framework_MockObject_MockObject */ protected $store; - /** @var StoreManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; - /** @var ResultFactory |\PHPUnit_Framework_MockObject_MockObject */ protected $resultFactory; @@ -86,15 +54,9 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var ResultLayout |\PHPUnit_Framework_MockObject_MockObject */ protected $resultLayout; - /** @var Layout |\PHPUnit_Framework_MockObject_MockObject */ - protected $layout; - /** @var RequestInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $request; - /** @var Wishlist |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlist; - /** @var ManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $messageManager; @@ -110,6 +72,15 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var EventManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $eventManager; + /** @var CaptchaHelper |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaHelper; + + /** @var CaptchaModel |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaModel; + + /** @var Session |\PHPUnit_Framework_MockObject_MockObject */ + protected $customerSession; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -136,7 +107,7 @@ protected function setUp() $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->setMethods([ 'getPost', - 'getPostValue', + 'getPostValue' ]) ->getMockForAbstractClass(); @@ -172,90 +143,72 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) - ->getMockForAbstractClass(); - - $this->wishlistConfig = $this->getMockBuilder(\Magento\Wishlist\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->transportBuilder = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) + $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) ->disableOriginalConstructor() + ->setMethods([ + 'getEmail', + 'getId' + ]) ->getMock(); - $this->inlineTranslation = $this->getMockBuilder(\Magento\Framework\Translate\Inline\StateInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getEmail') + ->willReturn('expamle@mail.com'); - $this->customerViewHelper = $this->getMockBuilder(\Magento\Customer\Helper\View::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getId') + ->willReturn(false); - $this->wishlistSession = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) + $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() - ->setMethods(['setSharingForm']) + ->setMethods([ + 'getCustomer', + 'getData' + ]) ->getMock(); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getCustomer') + ->willReturn($customerMock); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->setMethods(['getStoreId']) - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getData') + ->willReturn(false); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); + $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) + ->getMockForAbstractClass(); - $this->wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $this->captchaHelper = $this->getMockBuilder(CaptchaHelper::class) ->disableOriginalConstructor() ->setMethods([ - 'getShared', - 'setShared', - 'getId', - 'getSharingCode', - 'save', - 'isSalable', + 'getCaptcha' ]) ->getMock(); - $this->customerData = $this->getMockBuilder(\Magento\Customer\Model\Data\Customer::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + $this->captchaModel = $this->getMockBuilder(CaptchaModel::class) ->disableOriginalConstructor() ->setMethods([ - 'getBlock', - 'setWishlistId', - 'toHtml', + 'isRequired', + 'logAttempt' ]) ->getMock(); - $this->transport = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->getMockForAbstractClass(); + $objectHelper = new ObjectManager($this); + + $this->captchaHelper->expects($this->once())->method('getCaptcha') + ->willReturn($this->captchaModel); + $this->captchaModel->expects($this->any())->method('isRequired') + ->willReturn(false); - $this->model = new Send( - $this->context, - $this->formKeyValidator, - $this->customerSession, - $this->wishlistProvider, - $this->wishlistConfig, - $this->transportBuilder, - $this->inlineTranslation, - $this->customerViewHelper, - $this->wishlistSession, - $this->scopeConfig, - $this->storeManager + $this->model = $objectHelper->getObject( + Send::class, + [ + 'context' => $this->context, + 'formKeyValidator' => $this->formKeyValidator, + 'wishlistProvider' => $this->wishlistProvider, + 'captchaHelper' => $this->captchaHelper, + '_customerSession' => $this->customerSession + ] ); } @@ -291,409 +244,4 @@ public function testExecuteNoWishlistAvailable() $this->model->execute(); } - - /** - * @param string $text - * @param int $textLimit - * @param string $emails - * @param int $emailsLimit - * @param int $shared - * @param string $postValue - * @param string $errorMessage - * - * @dataProvider dataProviderExecuteWithError - */ - public function testExecuteWithError( - $text, - $textLimit, - $emails, - $emailsLimit, - $shared, - $postValue, - $errorMessage - ) { - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->once()) - ->method('getShared') - ->willReturn($shared); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($errorMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * 1. Text - * 2. Text limit - * 3. Emails - * 4. Emails limit - * 5. Shared wishlists counter - * 6. POST value - * 7. Error message (RESULT) - * - * @return array - */ - public function dataProviderExecuteWithError() - { - return [ - ['test text', 1, 'user1@example.com', 1, 0, '', 'Message length must not exceed 1 symbols'], - ['test text', 100, null, 1, 0, '', 'Please enter an email address.'], - ['test text', 100, '', 1, 0, '', 'Please enter an email address.'], - ['test text', 100, 'user1@example.com', 1, 1, '', 'This wish list can be shared 0 more times.'], - [ - 'test text', - 100, - 'u1@example.com, u2@example.com', - 3, - 2, - '', - 'This wish list can be shared 1 more times.' - ], - ['test text', 100, 'wrongEmailAddress', 1, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'user1@example.com, wrongEmailAddress', 2, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'wrongEmailAddress, user2@example.com', 2, 0, '', 'Please enter a valid email address.'], - ]; - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $text = 'test text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $rssLink = 'rss link'; - $sharingCode = 'sharing code'; - $exceptionMessage = 'test exception message'; - $postValue = ''; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with($shared) - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->layout->expects($this->once()) - ->method('getBlock') - ->with('wishlist.email.rss') - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('toHtml') - ->willReturn($rssLink); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->once()) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - // Throw Exception - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->willThrowException(new \Exception($exceptionMessage)); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($exceptionMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $text = 'text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $sharingCode = 'sharing code'; - $templateIdentifier = 'template identifier'; - $storeId = 1; - $viewOnSiteLink = 'view on site link'; - $from = 'user0@example.com'; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with(++$shared) - ->willReturnSelf(); - $this->wishlist->expects($this->exactly(2)) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('isSalable') - ->willReturn(true); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - - $this->layout->expects($this->exactly(2)) - ->method('getBlock') - ->willReturnMap([ - ['wishlist.email.rss', $this->layout], - ['wishlist.email.items', $this->layout], - ]); - - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->exactly(2)) - ->method('toHtml') - ->willReturn($text); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->exactly(2)) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - $this->scopeConfig->expects($this->exactly(2)) - ->method('getValue') - ->willReturnMap([ - ['wishlist/email/email_template', ScopeInterface::SCOPE_STORE, null, $templateIdentifier], - ['wishlist/email/email_identity', ScopeInterface::SCOPE_STORE, null, $from], - ]); - - $this->store->expects($this->once()) - ->method('getStoreId') - ->willReturn($storeId); - - $this->url->expects($this->once()) - ->method('getUrl') - ->with('*/shared/index', ['code' => $sharingCode]) - ->willReturn($viewOnSiteLink); - - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with([ - 'area' => Area::AREA_FRONTEND, - 'store' => $storeId, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with([ - 'customer' => $this->customerData, - 'customerName' => $customerName, - 'salable' => 'yes', - 'items' => $text, - 'viewOnSiteLink' => $viewOnSiteLink, - 'message' => $text . $text, - 'store' => $this->store, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setFrom') - ->with($from) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($emails) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('getTransport') - ->willReturn($this->transport); - - $this->transport->expects($this->once()) - ->method('sendMessage') - ->willReturnSelf(); - - $this->eventManager->expects($this->once()) - ->method('dispatch') - ->with('wishlist_share', ['wishlist' => $this->wishlist]) - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) - ->method('addSuccess') - ->with(__('Your wish list has been shared.')) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*', ['wishlist_id' => $wishlistId]) - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } } diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php new file mode 100644 index 0000000000000..fb0113eb6ae75 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\Model\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Wishlist\Model\Product\AttributeValueProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * AttributeValueProviderTest + */ +class AttributeValueProviderTest extends TestCase +{ + /** + * @var AttributeValueProvider|PHPUnit_Framework_MockObject_MockObject + */ + private $attributeValueProvider; + + /** + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactoryMock; + + /** + * @var Product|PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var AdapterInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->productCollectionFactoryMock = $this->createPartialMock( + CollectionFactory::class, + ['create'] + ); + $this->attributeValueProvider = new AttributeValueProvider( + $this->productCollectionFactoryMock + ); + } + + /** + * Get attribute text when the flat table is disabled + * + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + * @dataProvider attributeDataProvider + */ + public function testGetAttributeTextWhenFlatIsDisabled(int $productId, string $attributeCode, string $attributeText) + { + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getFirstItem' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(false); + $productCollection->expects($this->any()) + ->method('getFirstItem') + ->willReturn($this->productMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * Get attribute text when the flat table is enabled + * + * @dataProvider attributeDataProvider + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + */ + public function testGetAttributeTextWhenFlatIsEnabled(int $productId, string $attributeCode, string $attributeText) + { + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->any()) + ->method('fetchRow') + ->willReturn([ + $attributeCode => $attributeText + ]); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getConnection' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(true); + $productCollection->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * @return array + */ + public function attributeDataProvider(): array + { + return [ + [1, 'attribute_code', 'Attribute Text'] + ]; + } +} diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php index 85f6c504457d3..fc43baa0a67de 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php @@ -278,15 +278,35 @@ protected function processWishlistItemDescription($wishlistModelMock, $staticArg public function testIsAllowed() { + $customerId = 1; + $customerServiceMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class)->setMethods( + ['getId', '__wakeup', 'getCustomerId', 'getItemCollection', 'getSharingCode'] + )->disableOriginalConstructor()->getMock(); + $wishlist->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->wishlistHelperMock->expects($this->any())->method('getWishlist') + ->will($this->returnValue($wishlist)); + $this->wishlistHelperMock->expects($this->any()) + ->method('getCustomer') + ->will($this->returnValue($customerServiceMock)); + $customerServiceMock->expects($this->once())->method('getId')->willReturn($customerId); $this->scopeConfig->expects($this->once())->method('isSetFlag') ->with('rss/wishlist/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ->will($this->returnValue(true)); + $this->assertTrue($this->model->isAllowed()); } public function testGetCacheKey() { - $this->assertEquals('rss_wishlist_data', $this->model->getCacheKey()); + $wishlistId = 1; + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class)->setMethods( + ['getId', '__wakeup', 'getCustomerId', 'getItemCollection', 'getSharingCode'] + )->disableOriginalConstructor()->getMock(); + $wishlist->expects($this->once())->method('getId')->willReturn($wishlistId); + $this->wishlistHelperMock->expects($this->any())->method('getWishlist') + ->will($this->returnValue($wishlist)); + $this->assertEquals('rss_wishlist_data_1', $this->model->getCacheKey()); } public function testGetCacheLifetime() diff --git a/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php new file mode 100644 index 0000000000000..5e4c6b39f3c36 --- /dev/null +++ b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\ViewModel; + +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * ViewModel for Wishlist Cart Block + */ +class AllowedQuantity implements ArgumentInterface +{ + /** + * @var StockRegistry + */ + private $stockRegistry; + + /** + * @var ItemInterface + */ + private $item; + + /** + * @param StockRegistry $stockRegistry + */ + public function __construct(StockRegistry $stockRegistry) + { + $this->stockRegistry = $stockRegistry; + } + + /** + * Set product configuration item + * + * @param ItemInterface $item + * @return self + */ + public function setItem(ItemInterface $item): self + { + $this->item = $item; + return $this; + } + + /** + * Get product configuration item + * + * @return ItemInterface + */ + public function getItem(): ItemInterface + { + return $this->item; + } + + /** + * Get min and max qty for wishlist form. + * + * @return array + */ + public function getMinMaxQty(): array + { + $product = $this->getItem()->getProduct(); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $params = []; + + $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); + } else { + $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; + } + + return $params; + } +} diff --git a/app/code/Magento/Wishlist/composer.json b/app/code/Magento/Wishlist/composer.json index ce43f6faae200..c9c74c8859e42 100644 --- a/app/code/Magento/Wishlist/composer.json +++ b/app/code/Magento/Wishlist/composer.json @@ -15,7 +15,9 @@ "magento/module-rss": "*", "magento/module-sales": "*", "magento/module-store": "*", - "magento/module-ui": "*" + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-captcha": "*" }, "suggest": { "magento/module-configurable-product": "*", diff --git a/app/code/Magento/Wishlist/etc/config.xml b/app/code/Magento/Wishlist/etc/config.xml index 6588c41a0a7dd..dd88e63bc90ad 100644 --- a/app/code/Magento/Wishlist/etc/config.xml +++ b/app/code/Magento/Wishlist/etc/config.xml @@ -19,5 +19,21 @@ <text_limit>255</text_limit> </email> </wishlist> + <captcha translate="label"> + <frontend> + <areas> + <share_wishlist_form> + <label>Share Wishlist Form</label> + </share_wishlist_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <share_wishlist_form>1</share_wishlist_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/Wishlist/etc/module.xml b/app/code/Magento/Wishlist/etc/module.xml index c5ece20d7956b..ab48ee89b7474 100644 --- a/app/code/Magento/Wishlist/etc/module.xml +++ b/app/code/Magento/Wishlist/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Customer"/> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml index 243a06062425a..d4c3cc7fadd84 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml @@ -13,10 +13,12 @@ </referenceBlock> <referenceContainer name="content"> <block class="Magento\Wishlist\Block\Customer\Wishlist" name="customer.wishlist" template="Magento_Wishlist::view.phtml" cacheable="false"> + <block class="Magento\Theme\Block\Html\Pager" name="wishlist_item_pager"/> <block class="Magento\Wishlist\Block\Rss\Link" name="wishlist.rss.link" template="Magento_Wishlist::rss/wishlist.phtml"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Items" name="customer.wishlist.items" as="items" template="Magento_Wishlist::item/list.phtml" cacheable="false"> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Image" name="customer.wishlist.item.image" template="Magento_Wishlist::item/column/image.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Info" name="customer.wishlist.item.name" template="Magento_Wishlist::item/column/name.phtml" cacheable="false"/> + <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column" name="customer.wishlist.item.review" template="Magento_Wishlist::item/column/review.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.price" template="Magento_Wishlist::item/column/price.phtml" cacheable="false"> <block class="Magento\Catalog\Pricing\Render" name="product.price.render.wishlist"> <arguments> @@ -39,6 +41,7 @@ </block> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.cart" template="Magento_Wishlist::item/column/cart.phtml" cacheable="false"> <arguments> + <argument name="allowedQuantityViewModel" xsi:type="object">Magento\Wishlist\ViewModel\AllowedQuantity</argument> <argument name="title" translate="true" xsi:type="string">Add to Cart</argument> </arguments> </block> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 848c6a76393f8..f296b950f3abb 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml @@ -11,7 +11,9 @@ /** @var \Magento\Wishlist\Model\Item $item */ $item = $block->getItem(); $product = $item->getProduct(); -$allowedQty = $block->getMinMaxQty(); +/** @var \Magento\Wishlist\ViewModel\AllowedQuantity $viewModel */ +$viewModel = $block->getData('allowedQuantityViewModel'); +$allowedQty = $viewModel->setItem($item)->getMinMaxQty(); ?> <?php foreach ($block->getChildNames() as $childName): ?> <?= /* @noEscape */ $block->getLayout()->renderElement($childName, false) ?> @@ -22,8 +24,8 @@ $allowedQty = $block->getMinMaxQty(); <div class="field qty"> <label class="label" for="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> - <input type="number" data-role="qty" id="qty[<?= /* @noEscape */ $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" - name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>"> + <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" + name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>" <?= $product->isSaleable() ? '' : 'disabled="disabled"' ?>> </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml index 17e2404ee23cf..5ab5bc5422e7e 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml @@ -17,6 +17,6 @@ $item = $block->getItem(); <span><?= $block->escapeHtml(__('Comment')) ?></span> </label> <div class="control"> - <textarea id="product-item-comment-<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>" placeholder="<?= /* @noEscape */ $this->helper('Magento\Wishlist\Helper\Data')->defaultCommentString() ?>" name="description[<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>]" title="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="product-item-comment"><?= ($block->escapeHtml($item->getDescription())) ?></textarea> + <textarea id="product-item-comment-<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>" placeholder="<?= /* @noEscape */ $this->helper('Magento\Wishlist\Helper\Data')->defaultCommentString() ?>" name="description[<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>]" title="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="product-item-comment" <?= $item->getProduct()->isSaleable() ? '' : 'disabled="disabled"' ?>><?= ($block->escapeHtml($item->getDescription())) ?></textarea> </div> </div> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml new file mode 100644 index 0000000000000..9120cc9fa684e --- /dev/null +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Wishlist\Block\Customer\Wishlist\Item\Column $block */ +$product = $block->getItem()->getProduct(); +?> +<?= $block->getReviewsSummaryHtml($product, 'short') ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml index 430ebd384c82b..ff01cb4532cc7 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml @@ -40,6 +40,7 @@ </div> <?php endif; ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" title="<?= $block->escapeHtmlAttr(__('Share Wish List')) ?>" class="action submit primary"> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/view.phtml b/app/code/Magento/Wishlist/view/frontend/templates/view.phtml index 8b2e1b1c9d808..4f4a1d302c150 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/view.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/view.phtml @@ -10,6 +10,7 @@ ?> <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow()) : ?> + <div class="toolbar wishlist-toolbar"><?= $block->getChildHtml('wishlist_item_pager'); ?></div> <?= ($block->getChildHtml('wishlist.rss.link')) ?> <form class="form-wishlist-items" id="wishlist-view-form" data-mage-init='{"wishlist":{ @@ -51,5 +52,6 @@ <input name="entity" value="<%- data.entity %>"> <% } %> </form> - </script> + </script> + <div class="toolbar wishlist-toolbar"><br><?= $block->getChildHtml('wishlist_item_pager'); ?></div> <?php endif ?> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index cab130f7c2104..b38c5c2cda3ad 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -63,6 +63,12 @@ define([ isFileUploaded = false, self = this; + if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq + this._updateAddToWishlistButton({}); + event.stopPropagation(); + + return; + } $(event.handleObj.selector).each(function (index, element) { if ($(element).is('input[type=text]') || $(element).is('input[type=email]') || @@ -83,7 +89,9 @@ define([ } }); - this.bindFormSubmit(isFileUploaded); + if (isFileUploaded) { + this.bindFormSubmit(); + } this._updateAddToWishlistButton(dataToAdd); event.stopPropagation(); }, @@ -154,18 +162,12 @@ define([ $.each(elementValue, function (key, option) { data[elementName + '[' + option + ']'] = option; }); + } else if (elementName.substr(elementName.length - 2) == '[]') { //eslint-disable-line eqeqeq, max-depth + elementName = elementName.substring(0, elementName.length - 2); + + data[elementName + '[' + elementValue + ']'] = elementValue; } else { - if (elementValue) { //eslint-disable-line no-lonely-if - if (elementName.substr(elementName.length - 2) == '[]') { //eslint-disable-line eqeqeq, max-depth - elementName = elementName.substring(0, elementName.length - 2); - - if (elementValue) { //eslint-disable-line max-depth - data[elementName + '[' + elementValue + ']'] = elementValue; - } - } else { - data[elementName] = elementValue; - } - } + data[elementName] = elementValue; } return data; @@ -187,45 +189,34 @@ define([ /** * Bind form submit. - * - * @param {Boolean} isFileUploaded */ - bindFormSubmit: function (isFileUploaded) { + bindFormSubmit: function () { var self = this; $('[data-action="add-to-wishlist"]').on('click', function (event) { var element, params, form, action; - if (!$($(self.options.qtyInfo).closest('form')).valid()) { - event.stopPropagation(); - event.preventDefault(); - - return; - } - - if (isFileUploaded) { + event.stopPropagation(); + event.preventDefault(); - element = $('input[type=file]' + self.options.customOptionsInfo); - params = $(event.currentTarget).data('post'); - form = $(element).closest('form'); - action = params.action; + element = $('input[type=file]' + self.options.customOptionsInfo); + params = $(event.currentTarget).data('post'); + form = $(element).closest('form'); + action = params.action; - if (params.data.id) { - $('<input>', { - type: 'hidden', - name: 'id', - value: params.data.id - }).appendTo(form); - } - - if (params.data.uenc) { - action += 'uenc/' + params.data.uenc; - } + if (params.data.id) { + $('<input>', { + type: 'hidden', + name: 'id', + value: params.data.id + }).appendTo(form); + } - $(form).attr('action', action).submit(); - event.stopPropagation(); - event.preventDefault(); + if (params.data.uenc) { + action += 'uenc/' + params.data.uenc; } + + $(form).attr('action', action).submit(); }); } }); diff --git a/app/code/Magento/WishlistAnalytics/composer.json b/app/code/Magento/WishlistAnalytics/composer.json index fc69afe2907ab..747f2a4baaaa9 100644 --- a/app/code/Magento/WishlistAnalytics/composer.json +++ b/app/code/Magento/WishlistAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index e3a788af2ea7e..792928ab61aaf 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -13,6 +13,7 @@ use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; use Magento\Wishlist\Model\Wishlist; use Magento\Wishlist\Model\WishlistFactory; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; /** * Fetches the Wishlist data according to the GraphQL schema @@ -51,6 +52,10 @@ public function resolve( ) { $customerId = $context->getUserId(); + /* Guest checking */ + if (!$customerId && 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } /** @var Wishlist $wishlist */ $wishlist = $this->wishlistFactory->create(); $this->wishlistResource->load($wishlist, $customerId, 'customer_id'); diff --git a/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less index c1b684aef354f..afd91ed3dbde6 100644 --- a/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less @@ -83,7 +83,7 @@ .message-system-short-wrapper { overflow: hidden; - padding: 0 1.5rem 0 @indent__l; + padding: 0 1.5rem 0 1rem; } .message-system-collapsible { diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less index 832c66b7988e0..bf7ee7850f9d0 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less @@ -42,4 +42,5 @@ color: @page-title__color; font-size: @page-title__font-size; margin-bottom: 0; + word-break: break-all; } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less index 08434727ccc9c..8499ecaa48c12 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less @@ -45,6 +45,10 @@ .page-actions { @_page-action__indent: 1.3rem; + &.floating-header { + &:extend(.page-actions-buttons all); + } + .page-main-actions & { &._fixed { left: @page-wrapper__indent-left; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less index e8e2746717e6a..dec35d1364836 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less @@ -162,9 +162,12 @@ &.collapsible-block-wrapper-last { border-bottom: 0; } + .admin__dynamic-rows.admin__control-collapsible { - .admin__collapsible-block-wrapper { - border-bottom: none; + td { + &.admin__collapsible-block-wrapper { + border-bottom: none; + } } } } @@ -342,7 +345,7 @@ } .value { - padding-right: 4rem; + padding-right: 2rem; } } @@ -492,6 +495,8 @@ width: 44%; &.with-tooltip { + font-size: 0; + .tooltip { bottom: 0; float: right; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less index 42b3ecfb71122..070ee6347508f 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less @@ -203,7 +203,7 @@ font-weight: @font-weight__heavier; line-height: @line-height__s; margin: 0 0 -1px; - padding: @admin__page-nav-link__padding; + padding: 2rem 0 2rem 1rem; transition: @admin__page-nav-transition; word-wrap: break-word; } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less index c0c94eaaf3507..05e6350c88d8e 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less @@ -38,7 +38,7 @@ .admin__legend { .admin__field-tooltip { margin-left: -@indent__base; - margin-top: -.2rem; + margin-top: .5rem; } } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less index ad407160034ac..22a584f1c8b80 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less @@ -235,6 +235,7 @@ .store-view { &:not(.store-switcher) { float: left; + margin-top: 13px; } .store-switcher-label { diff --git a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less index 3355950254072..ffbbaeb084162 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less @@ -15,6 +15,14 @@ } } +.catalog-category-edit { + .admin__grid-control { + .admin__grid-control-value { + display: none; + } + } +} + .product-composite-configure-inner { .admin__control-text { &.qty { diff --git a/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less index 17be2ca706076..08606402f7a0e 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less @@ -34,7 +34,7 @@ .admin__field-control { direction: rtl; display: inline-block; - margin: -4px 0 0; + margin: -1px 0 0; unicode-bidi: bidi-override; vertical-align: top; width: 125px; diff --git a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less index c405707ee7bbe..16c84047b529d 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less @@ -7,13 +7,21 @@ .rma-request-details, .rma-wrapper .order-shipping-address { float: left; - #mix-grid .width(6,12); + /** + * @codingStandardsIgnoreStart + */ + #mix-grid .width(6, 12); + //@codingStandardsIgnoreEnd } .rma-confirmation, - .rma-wrapper .order-return-address { + .rma-wrapper .order-return-address, .rma-wrapper .order-shipping-method { float: right; - #mix-grid .width(6,12); + /** + * @codingStandardsIgnoreStart + */ + #mix-grid .width(6, 12); + //@codingStandardsIgnoreEnd } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less index 1e76679f594c1..fa1ae25628986 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less @@ -92,6 +92,14 @@ margin: 0; padding: 0; } + .admin__data-grid-pager-wrap{ + .selectmenu { + margin-bottom: 10px; + } + } + .data-grid-search-control-wrap { + margin-bottom: 10px; + } } // diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less index e14bcbcddd47f..f66e94940c55d 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less @@ -27,3 +27,19 @@ width: 50%; } } + +.page-create-order { + .order-details { + &:not(.order-details-existing-customer) { + .order-account-information { + .field-email { + margin-left: -30px; + } + + .field-group_id { + margin-right: 30px; + } + } + } + } +} diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less index 2f6aec0315e3b..5bcf4d4953cc6 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less @@ -49,7 +49,7 @@ margin: 0 0 @order-create-sidebar__margin; .lib-typography( @_font-size: 1.9rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__semibold, @_line-height: @line-height__s, @_font-family: false, diff --git a/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less b/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less index 3e1f4e75031d2..ae13c479cea41 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less +++ b/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less @@ -16,7 +16,7 @@ @staging-preview-header__font-size: 1.3rem; @staging-preview-header-item__active__background-color: @color-brownie-almost; -@staging-preview-header-item-actions__border-color: @color-darkie-gray; +@staging-preview-header-item-actions__border-color: @color-darker-gray; @staging-preview-form-element__background-color: @color-very-dark-brownie; @staging-preview-form-element__border-color: @color-lighter-grayish-almost; diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less index d55608ade4a05..946d11db2d1a2 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less @@ -392,6 +392,7 @@ body._in-resize { overflow: hidden; padding: 0; vertical-align: top; + vertical-align: middle; width: @control-checkbox-radio__size + @data-grid-checkbox-cell-inner__padding-horizontal * 2; &:hover { @@ -1074,8 +1075,10 @@ body._in-resize { } .data-grid-checkbox-cell-inner { - margin: @data-grid-checkbox-cell-inner__padding-top @data-grid-checkbox-cell-inner__padding-horizontal .9rem; + display: unset; + margin: 0 @data-grid-checkbox-cell-inner__padding-horizontal 0; padding: 0; + text-align: center; } // Content Hierarchy specific diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less index 6e03e1d0cebaa..e37e08f3b667d 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less @@ -99,7 +99,7 @@ } .action-menu { - max-height: 3.85rem * @data-grid-search-control-action-menu-item__quantity; // ToDo UI: change static item height + max-height: 3.85rem * @data-grid-search-control-action-menu-item__quantity; // @todo: change static item height overflow-y: auto; z-index: @data-grid-search-menu__z-index; } @@ -354,6 +354,7 @@ .admin__current-filters-list-wrap { width: 100%; + word-break: break-all; } .admin__current-filters-list { diff --git a/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less index 1cd867efdd13b..554b6394a1094 100644 --- a/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less @@ -68,12 +68,14 @@ a { color: @color-gray85; + cursor: move; display: block; float: left; text-decoration: none; } a:last-child { + cursor: pointer; float: right; } } diff --git a/app/design/adminhtml/Magento/backend/etc/view.xml b/app/design/adminhtml/Magento/backend/etc/view.xml index f10f7789b0888..18c2d8f1b1722 100644 --- a/app/design/adminhtml/Magento/backend/etc/view.xml +++ b/app/design/adminhtml/Magento/backend/etc/view.xml @@ -23,6 +23,8 @@ </images> </media> <exclude> + <item type="file">Lib::mage/captcha.js</item> + <item type="file">Lib::mage/captcha.min.js</item> <item type="file">Lib::mage/common.js</item> <item type="file">Lib::mage/cookies.js</item> <item type="file">Lib::mage/dataPost.js</item> diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less index 0049022204619..8b26177a05cc8 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less @@ -10,7 +10,7 @@ @tooltip__background-color: @color-white; @tooltip__border-color: @color-gray68; @tooltip__border-radius: 0; -@tooltip__color: @color-brown-darkie; +@tooltip__color: @color-brown-darker; @tooltip__max-width: 31rem; @tooltip__opacity: .9; @tooltip__shadow-color: @color-gray80; diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less index 39d7be029f81f..be1378638180f 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less @@ -28,7 +28,7 @@ @color-green-apple: #79a22e; @color-green-islamic: #090; @color-dark-brownie: #41362f; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-phoenix-down: #e04f00; @color-phoenix: #eb5202; @color-phoenix-almost-rise: #ef672f; diff --git a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less index 911ef55f3f2e6..30500569c82a0 100644 --- a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less +++ b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less @@ -15,7 +15,7 @@ @extension-manager-title__background-color: @color-white-fog; @extension-manager-title__border-color: @color-gray89; -@extension-manager-title__color: @color-brown-darkie; +@extension-manager-title__color: @color-brown-darker; @extension-manager-button__border-color: @color-gray68; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less b/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less index 475d3914a5ff0..5658214a76986 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less @@ -45,13 +45,13 @@ } .ui-tabs-anchor { - color: @color-brown-darkie; + color: @color-brown-darker; display: block; padding: 1.5rem 1.8rem 1.3rem; text-decoration: none; &:hover { // ToDo UI: should be deleted with old styles - color: @color-brown-darkie; + color: @color-brown-darker; text-decoration: none; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_typography.less b/app/design/adminhtml/Magento/backend/web/css/source/_typography.less index 54726d2d34bd9..1f7d7f879c4aa 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_typography.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_typography.less @@ -71,7 +71,7 @@ h1 { .lib-typography( @_font-size: 2.8rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__regular, @_line-height: @line-height__s, @_font-family: false, @@ -84,7 +84,7 @@ h2 { .lib-typography( @_font-size: 2rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__regular, @_line-height: @line-height__s, @_font-family: false, @@ -97,7 +97,7 @@ h3 { .lib-typography( @_font-size: 1.7rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__semibold, @_line-height: @line-height__s, @_font-family: false, diff --git a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-dropdown.less b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-dropdown.less index cd089232412dc..d1fe33c4fe77d 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-dropdown.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-dropdown.less @@ -234,6 +234,7 @@ border: 0; display: inline; margin: 0; + width: 6rem; body._keyfocus &:focus { box-shadow: none; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less index 4c364bed688bc..61bd94cf3f49c 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less @@ -338,7 +338,7 @@ border-top: @action-multiselect-tree-lines; height: 1px; top: @action-multiselect-menu-item__padding + @action-multiselect-tree-arrow__size/2; - width: @action-multiselect-tree-menu-item__margin-left + @action-multiselect-menu-item__padding; + width: @action-multiselect-tree-menu-item__margin-left; } // Vertical dotted line diff --git a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less index 1cc61bef5da07..1c45fe6946ba0 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less @@ -7,6 +7,14 @@ // Actions -> Action select // _____________________________________________ +// +// Variables +// _____________________________________________ + +@_dropdown__padding-right: @action__height; +@_triangle__height: @button-marker-triangle__height; +@_triangle__width: @button-marker-triangle__width; + // Action select have the same visual styles and functionality as native <select> .action-select-wrap { @_action-select__border-color: @button__border-color; @@ -18,9 +26,9 @@ .action-select { .action-toggle-triangle( - @_dropdown__padding-right: @_action-select-toggle__size; - @_triangle__height: @button-marker-triangle__height; - @_triangle__width: @button-marker-triangle__width; + @_dropdown__padding-right; + @_triangle__height; + @_triangle__width; ); .lib-text-overflow-ellipsis(); @@ -108,12 +116,9 @@ min-width: 100%; position: static; - ._parent._visible { - position: relative; - } - .action-submenu { position: absolute; + right: -100%; } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less index 88962a1019a19..84d9cb1530893 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less @@ -44,6 +44,7 @@ margin: 0 @indent__xs 15px 0; overflow: hidden; padding: 3px; + text-overflow: ellipsis; width: 100px; &.selected { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less index 9a88d5e3593b9..ec276449263a4 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less @@ -48,7 +48,7 @@ @data-grid-file-uploader-menu-button__width: 2rem; -@data-grid-file-uploader-upload-icon__color: @color-darkie-gray; +@data-grid-file-uploader-upload-icon__color: @color-darker-gray; @data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray; @data-grid-file-uploader-upload-icon__line-height: 48px; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less index de24bf89620d4..15cd295885892 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less @@ -76,7 +76,8 @@ position: absolute; speak: none; text-shadow: none; - top: 1.3rem; + top: 50%; + margin-top: -1.25rem; width: auto; } } @@ -110,7 +111,7 @@ content: @alert-icon__error__content; font-size: @alert-icon__error__font-size; left: 2.2rem; - margin-top: 0.5rem; + margin-top: -1.1rem; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less index 95d7f8f65fdc1..efc747e4d714a 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less @@ -146,13 +146,13 @@ } .action-close { - padding: @modal-popup__padding; + padding: @modal-popup__padding - 2; &:active, &:focus { background: transparent; - padding-right: @modal-popup__padding + (@modal-action-close__font-size - @modal-action-close__active__font-size) / 2; - padding-top: @modal-popup__padding + (@modal-action-close__font-size - @modal-action-close__active__font-size) / 2; + padding-right: @modal-popup__padding - 2; + padding-top: @modal-popup__padding - 2; } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less index d1d1ff9891634..bcdc96b6c1754 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less @@ -264,7 +264,7 @@ } } - #contents-uploader { + .contents-uploader { margin: 0 0 @indent__base; } @@ -299,6 +299,7 @@ margin: 0 @indent__xs 15px 0; overflow: hidden; padding: 3px; + text-overflow: ellipsis; width: 100px; &.selected { @@ -310,7 +311,7 @@ } } - #contents-uploader { + .contents-uploader { &:extend(.abs-clearfix all); } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less index f971246ab469d..6c3756370d9ce 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less @@ -85,7 +85,7 @@ cursor: pointer; } - &:focus { + &:active { background-image+: url('../images/arrows-bg.svg'); background-position+: ~'calc(100% - 12px)' 13px; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index 02925881253ea..66c9086c15aa7 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -81,7 +81,7 @@ min-width: 0; padding: 0; - // Filedset section + // Fieldset section .fieldset-wrapper { &.admin__fieldset-section { > .fieldset-wrapper-title { @@ -121,6 +121,9 @@ > .admin__field-control { #mix-grid .column(@field-control-grid__column, @field-grid__columns); + input[type="checkbox"] { + margin-top: @indent__s; + } } > .admin__field-label { @@ -156,6 +159,14 @@ } } } + &.composite-bundle { + .admin__field-control { + padding-top: 7px; + } + .admin__field-option { + padding-top: 0; + } + } } .admin__fieldset-product-websites { @@ -531,7 +542,6 @@ & > .admin__field { &:first-child { position: static; - & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); cursor: pointer; @@ -649,10 +659,11 @@ &.admin__field { > .admin__field-control { &:extend(.abs-field-size-small all); - float: left; position: relative; + display: inline-block; } } + + .admin__field:last-child { width: auto; @@ -685,6 +696,7 @@ margin: 0; opacity: 1; position: static; + width: 100%; } } & > .admin__field-label { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less index df031bebeb24a..71f57b765ff0e 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less @@ -11,7 +11,7 @@ .admin__fieldset-wrapper-title { &:extend(.abs-clearfix all); border-bottom: 1px solid @color-gray80; - line-height: 1.2; + line-height: 1.4; margin-bottom: 0; padding: 14px 0 16px; @@ -162,7 +162,7 @@ @_icon-font-line-height: 16px, @_icon-font-text-hide: true, @_icon-font-position: after, - @_icon-font-color: @color-brown-darkie + @_icon-font-color: @color-brown-darker ); span { @@ -175,7 +175,7 @@ z-index: 2; &:after { - color: darken(@color-brown-darkie, 20%); + color: darken(@color-brown-darker, 20%); } // @Todo ui - testing solution to show action hint without title attribute @@ -253,7 +253,7 @@ label.mage-error { .captcha-reload { float: right; - vertical-align: middle; + margin-top: 15px; } } } @@ -470,8 +470,6 @@ label.mage-error { } .admin__data-grid-header-row { - &:extend(.abs-cleafix); - .action-select-multiselect { -webkit-appearance: menulist-button; appearance: menulist-button; @@ -552,7 +550,7 @@ label.mage-error { } .admin__control-select-placeholder { - color: @color-darkie-gray; + color: @color-darker-gray; font-weight: @font-weight__bold; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less index a9035a9a7e47d..697d11fb57d67 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less @@ -118,7 +118,7 @@ } &._fit { - width: 1px; + width: auto; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less old mode 100644 new mode 100755 index 8184a5c4bb248..befd27fa57df6 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less @@ -22,7 +22,7 @@ @field-tooltip-content__width: 32rem; @field-tooltip-content__z-index: 1; -@field-tooltip-action__margin-left: 2rem; +@field-tooltip-action__margin-left: 0; // // Form Fields diff --git a/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less b/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less index b477384096b01..ad57d7b47113e 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less @@ -8,7 +8,7 @@ // _____________________________________________ @color-brown-dark: #4a3f39; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-very-dark-gray-black: #303030; @color-very-dark-gray-black2: #35302c; @color-very-dark-grayish-orange: #373330; @@ -23,7 +23,7 @@ @color-brownie-vanilla: #736963; @color-dark-gray0: #7f7c7a; @color-dark-gray: #808080; -@color-darkie-gray: #8a837f; +@color-darker-gray: #8a837f; @color-gray65: #a6a6a6; @color-gray65-almost: #a79d95; @color-gray65-lighten: #aaa6a0; @@ -73,5 +73,5 @@ @primary__color: @color-phoenix; @success__color: @color-green-apple; -@text__color: @color-brown-darkie; +@text__color: @color-brown-darker; @border__color: @color-gray89; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less b/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less index 69393a62200cc..40831684adceb 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less @@ -30,7 +30,7 @@ @data-grid-td__odd__update__active__background-color: darken(@data-grid-td__update__active__background-color, 10%); @data-grid-td__odd__update__upcoming__background-color: darken(@data-grid-td__update__upcoming__background-color, 10%); -@data-grid-th__border-color: @color-darkie-gray; +@data-grid-th__border-color: @color-darker-gray; @data-grid-th__border-style: solid; @data-grid-th__background-color: @color-brownie; @data-grid-th__color: @color-white; diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 26381367c72f5..2dbe68ef96eec 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -2738,7 +2738,8 @@ // --------------------------------------------- #widget_instace_tabs_properties_section_content .widget-option-label { - margin-top: 6px; + margin-top: 7px; + display: inline-block; } // @@ -3845,6 +3846,26 @@ .rule-param-edit .element { display: inline; + position: relative; + } + + .rule-param-edit .element input.input-date, + .rule-param-edit .element input.input-date[readonly] { + background-color: @color-white; + min-width: 140px; + width: 140px !important; + cursor: pointer; + text-align: center; + opacity: 1; + margin-right: 10px; + padding-right: 40px; + + + .ui-datepicker-trigger { + position: absolute; + width: 140px; + text-align: right; + left: 0; + } } .rule-param-edit .element .addafter { diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index 08a9b61977922..299c138832064 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -488,6 +488,7 @@ .product-items-names { .product-item { + display: flex; margin-bottom: @indent__s; } @@ -506,17 +507,6 @@ min-height: inherit; } } - - // - // Category page 1 column layout - // --------------------------------------------- - - .catalog-category-view.page-layout-1column { - .column.main { - min-height: inherit; - } - } - } // diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less index 42b1bf2d0cc09..7181606090ccb 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less @@ -23,6 +23,15 @@ } .block.widget { + .products-grid .product-item { + margin-left: 2%; + width: calc(~'(100% - 2%)/2'); + + &:nth-child(2n + 1) { + margin-left: 0; + } + } + .product-item-info { width: auto; } @@ -60,6 +69,15 @@ .page-layout-3columns .block.widget .products-grid .product-item { width: 100%/3; } + + .page-layout-1column .block.widget .products-grid .product-item { + margin-left: 2%; + width: calc(~'(100% - 4%)/3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } + } } // @@ -82,7 +100,16 @@ } .page-layout-1column .block.widget .products-grid .product-item { - width: 100%/4; + margin-left: 2%; + width: calc(~'(100% - 6%)/4'); + + &:nth-child(3n + 1) { + margin-left: 2%; + } + + &:nth-child(4n + 1) { + margin-left: 0; + } } .page-layout-3columns .block.widget .products-grid .product-item { @@ -96,11 +123,11 @@ } .page-layout-1column .block.widget .products-grid .product-item { - margin-left: calc(~'(100% - 5 * (100%/6)) / 4'); - width: 100%/6; + margin-left: 2%; + width: calc(~'(100% - 8%)/5'); &:nth-child(4n + 1) { - margin-left: calc(~'(100% - 5 * (100%/6)) / 4'); + margin-left: 2%; } &:nth-child(5n + 1) { diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less index 951ca89a07988..b7af69fd5ca82 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less @@ -29,15 +29,23 @@ .product { &-items { + font-size: 0; &:extend(.abs-reset-list all); } &-item { + font-size: 1.4rem; vertical-align: top; .products-grid & { display: inline-block; - width: 100%/2; + margin-left: 2%; + padding: 0; + width: calc(~'(100% - 2%) / 2'); + } + + &:nth-child(2n + 1) { + margin-left: 0; } &:extend(.abs-add-box-sizing all); @@ -63,13 +71,26 @@ } &-actions { + font-size: 0; + + > * { + font-size: 1.4rem; + } .actions-secondary { + display: inline-block; + font-size: 1.4rem; + vertical-align: middle; + white-space: nowrap; > button.action { .lib-button-reset(); } > .action { + line-height: 35px; + text-align: center; + width: 35px; + &:extend(.abs-actions-addto-gridlist all); &:before { margin: 0; @@ -80,6 +101,10 @@ } } } + + .actions-primary { + display: inline-block; + } } &-description { @@ -191,19 +216,6 @@ } } - .column.main { - .product { - &-items { - margin-left: -@indent__base; - } - - &-item { - padding-left: @indent__base; - } - } - - } - .price-container { .price { .lib-font-size(14); @@ -302,18 +314,10 @@ } .actions-primary + .actions-secondary { - display: table-cell; - padding-left: 5px; - white-space: nowrap; - width: 50%; > * { white-space: normal; } } - - .actions-primary { - display: table-cell; - } } } } @@ -329,7 +333,13 @@ .page-products.page-layout-3columns { .products-grid { .product-item { - width: 100%/3; + margin-left: 2%; + padding: 0; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } @@ -343,7 +353,13 @@ .page-products { .products-grid { .product-item { - width: 100%/3; + margin-left: 2%; + padding: 0; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } @@ -394,9 +410,13 @@ } .product-item { - margin-left: calc(~'(100% - 4 * 23.233%) / 3'); + margin-left: 2%; padding: 0; - width: 23.233%; + width: calc(~'(100% - 6%) / 4'); + + &:nth-child(3n + 1) { + margin-left: 2%; + } &:nth-child(4n + 1) { margin-left: 0; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less index 673131563417d..65f3eeef63b01 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less @@ -342,7 +342,7 @@ .item-qty { margin-right: @indent__s; text-align: center; - width: 40px; + width: 45px; } .update-cart-item { diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less index 4479c070a4e17..8dec680b58726 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less @@ -55,31 +55,3 @@ } } } - -// -// Desktop -// _____________________________________________ - -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { - // ToDo UI: remove with global blank theme .field.required update - .opc-wrapper { - .fieldset { - > .field { - &.required, - &._required { - position: relative; - - > label { - padding-right: 25px; - - &:after { - margin-left: @indent__s; - position: absolute; - top: 9px; - } - } - } - } - } - } -} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 43c2ad50c7a6f..3394e8a4b50cf 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -118,6 +118,10 @@ .product { position: relative; + .item-options { + &:extend(.abs-product-options-list all); + &:extend(.abs-add-clearfix all); + } } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less index 3ce46a73a11c4..4d04b6e0b9653 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less @@ -137,6 +137,7 @@ } } + .captcha, .number { .input-text { width: 225px; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less index 5f8134193c67f..35445b0989e86 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -209,6 +209,13 @@ .fieldset { > .field { margin: 0 0 @indent__base; + + &.choice { + &:before { + padding: 0; + width: 0; + } + } &.type { .control { diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less index b54c0a264a03a..0f2a7abcbaa18 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less @@ -67,3 +67,11 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { + .opc-block-shipping-information { + .shipping-information-title { + font-size: 2.3rem; + } + } +} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less index bf264a98f33b8..423923d5e6457 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less @@ -55,6 +55,10 @@ } } + .label { + .lib-visually-hidden(); + } + .field-tooltip-action { .lib-icon-font( @checkout-tooltip-icon__content, @@ -65,6 +69,10 @@ @_icon-font-color-active: false ); + &:before { + padding-left : 1px; + } + &:focus { ._keyfocus & { .lib-css(z-index, @checkout-tooltip__hover__z-index); @@ -147,3 +155,32 @@ } } } + +// +// Tablet +// _____________________________________________ + +@media only screen and (max-width: @screen__m) { + .field-tooltip .field-tooltip-content { + left: auto; + right: -10px; + top: 40px; + } + .field-tooltip .field-tooltip-content::before, + .field-tooltip .field-tooltip-content::after { + border: 10px solid transparent; + height: 0; + left: auto; + margin-top: -21px; + right: 10px; + top: 0; + width: 0; + } + .field-tooltip .field-tooltip-content::before { + .lib-css(border-bottom-color, @checkout-tooltip-content__border-color); + } + .field-tooltip .field-tooltip-content::after { + .lib-css(border-bottom-color, @checkout-tooltip-content__background-color); + top: 1px; + } +} diff --git a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less index 9df59ca5dac92..213b8131815b3 100644 --- a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less @@ -165,7 +165,7 @@ // Checkout address (create shipping address) .field.street { - .field.additional { + .field { .label { &:extend(.abs-visually-hidden all); } @@ -367,8 +367,8 @@ } .account { - .page.messages { - margin-bottom: @indent__base; + .messages { + margin-bottom: 0; } .toolbar { @@ -388,6 +388,17 @@ position: relative; } } + + .form.search.advanced { + .field.price { + .with-addon { + .input-text { + flex-basis: auto; + width: 100%; + } + } + } + } } // @@ -421,7 +432,7 @@ > .field { > .control { - width: 55%; + width: 80%; } } } @@ -451,7 +462,9 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.search.advanced, + .form.form-orders-search { min-width: 600px; width: 50%; } diff --git a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less index f0dd8a957e9b5..6e2069c6e88ef 100644 --- a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less @@ -55,6 +55,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price { text-decoration: none; diff --git a/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less index 2761a2f74f990..c572c983d80d9 100644 --- a/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less @@ -350,7 +350,7 @@ .product { &-item { &-checkbox { - left: 20px; + left: 0; position: absolute; top: 20px; } diff --git a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less index 215d7d8b322b4..b189d4e08ba17 100644 --- a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less +++ b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less @@ -68,8 +68,10 @@ } // Remove address and phone number link color on iOS -.address-details a { - &:extend(.no-link a); +.email-non-inline() { + .address-details a { + &:extend(.no-link a); + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__xs) { diff --git a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less index 3847393a2f046..298ccbf58e687 100644 --- a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less @@ -294,6 +294,14 @@ } } } + .order-items.table-wrapper { + .col.price, + .col.qty, + .col.subtotal, + .col.msrp { + text-align: left; + } + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { diff --git a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less index 28aa3f187e95c..b7271e3c1e248 100644 --- a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less @@ -91,15 +91,19 @@ &-options { margin-top: @indent__s; + &:focus { + box-shadow: none; + } + .swatch-option-tooltip-layered .title { .lib-css(color, @swatch-option-tooltip-layered-title__color); - width: 100%; - height: 20px; - position: absolute; bottom: -5px; + height: 20px; left: 0; - text-align: center; margin-bottom: @indent__s; + position: absolute; + text-align: center; + width: 100%; } } @@ -110,7 +114,7 @@ .lib-css(color, @attr-swatch-option__color); &.selected { - .lib-css(blackground, @attr-swatch-option__selected__background); + .lib-css(background, @attr-swatch-option__selected__background); .lib-css(border, @attr-swatch-option__selected__border); .lib-css(color, @attr-swatch-option__selected__color); } @@ -132,15 +136,19 @@ text-align: center; text-overflow: ellipsis; + &:focus { + box-shadow: @focus__box-shadow; + } + &.text { .lib-css(background, @swatch-option-text__background); .lib-css(color, @swatch-option-text__color); font-size: @font-size__s; font-weight: @font-weight__bold; line-height: 20px; - padding: 4px 8px; - min-width: 22px; margin-right: 7px; + min-width: 22px; + padding: 4px 8px; &.selected { .lib-css(background-color, @swatch-option-text__selected__background-color) !important; diff --git a/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less index 0e8350261e002..9cd0439c13956 100644 --- a/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less @@ -8,6 +8,24 @@ // _____________________________________________ & when (@media-common = true) { + .toolbar { + &.wishlist-toolbar { + .limiter { + float: right; + } + .main .pages { + display: inline-block; + position: relative; + z-index: 0; + } + .toolbar-amount, + .limiter { + display: inline-block; + z-index: 1; + } + } + } + .form.wishlist.items { .actions-toolbar { &:extend(.abs-reset-left-margin all); @@ -177,10 +195,10 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .products-grid.wishlist { margin-bottom: @indent__l; - margin-right: -@indent__s; + margin-right: 0; .product { &-item { - padding: @indent__base @indent__s @indent__base @indent__base; + padding: @indent__base 0 @indent__base 0; position: relative; &-photo { @@ -194,6 +212,7 @@ &-actions { display: block; + float: left; .action { margin-right: 15px; diff --git a/app/design/frontend/Magento/blank/etc/view.xml b/app/design/frontend/Magento/blank/etc/view.xml index 572632b6683e3..e742ce0a21cd1 100644 --- a/app/design/frontend/Magento/blank/etc/view.xml +++ b/app/design/frontend/Magento/blank/etc/view.xml @@ -250,7 +250,7 @@ <var name="product_list_image_size">166</var> <!-- New Product image size used in product list --> <var name="product_zoom_image_size">370</var> <!-- New Product image size used for zooming --> - <var name="product_image_white_borders">0</var> + <var name="product_image_white_borders">1</var> </vars> <vars module="Magento_Bundle"> <var name="product_summary_image_size">58</var> <!-- New Product image size used for summary block--> diff --git a/app/design/frontend/Magento/blank/web/css/source/_forms.less b/app/design/frontend/Magento/blank/web/css/source/_forms.less index 94b993b53b508..26f5ff89e99e3 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_forms.less +++ b/app/design/frontend/Magento/blank/web/css/source/_forms.less @@ -18,7 +18,7 @@ .fieldset { .lib-form-fieldset(); &:last-child { - margin-bottom: 0; + margin-bottom: @indent__base; } > .field, @@ -101,6 +101,18 @@ .lib-form-validation-note(); } + .product-options-wrapper { + .date { + &.required { + div[for*='options'] { + &.mage-error { + display: none !important; + } + } + } + } + } + .field .tooltip { .lib-tooltip(right); .tooltip-content { diff --git a/app/design/frontend/Magento/blank/web/css/source/_navigation.less b/app/design/frontend/Magento/blank/web/css/source/_navigation.less index 4499886ef0f10..21b7315779764 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_navigation.less +++ b/app/design/frontend/Magento/blank/web/css/source/_navigation.less @@ -131,12 +131,18 @@ ); } } - .switcher-dropdown { .lib-list-reset-styles(); + display: none; padding: @indent__s 0; } - + .switcher-options { + &.active { + .switcher-dropdown { + display: block; + } + } + } .header.links { .lib-list-reset-styles(); border-bottom: 1px solid @color-gray82; @@ -207,7 +213,7 @@ } .nav-toggle { - &:after{ + &:after { background: rgba(0, 0, 0, @overlay__opacity); content: ''; display: block; diff --git a/app/design/frontend/Magento/blank/web/css/source/_sections.less b/app/design/frontend/Magento/blank/web/css/source/_sections.less index f0a3518c92a8b..1eee47bda817c 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_sections.less +++ b/app/design/frontend/Magento/blank/web/css/source/_sections.less @@ -31,8 +31,19 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .product.data.items { .lib-data-accordion(); + .data.item { display: block; } + + .item.title { + > .switch { + padding: 1px 15px 1px; + } + } + + > .item.content { + padding: 10px 15px 30px; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less index 43ae23bab7895..45a01269bef66 100644 --- a/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less @@ -58,6 +58,7 @@ .field.choice { input { float: left; + margin-top: 4px; } .label { @@ -253,7 +254,7 @@ .box-tocart { .action.primary { margin-right: 1%; - width: 49%; + width: auto; } } diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index 501a1d2918d6a..3b4da1d1ae6f5 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -294,6 +294,12 @@ } .product-options-wrapper { + .fieldset { + &:focus { + box-shadow: none; + } + } + .fieldset-product-options-inner { .legend { .lib-css(font-weight, @font-weight__semibold); @@ -391,6 +397,7 @@ .box-tocart { &:extend(.abs-box-tocart all); + .field.qty { } @@ -534,6 +541,15 @@ } } + .block-compare { + .action { + &.delete { + &:extend(.abs-remove-button-for-blocks all); + right: initial; + } + } + } + .action.tocart { border-radius: 0; } @@ -563,6 +579,7 @@ .product-items-names { .product-item { + display: flex; margin-bottom: @indent__s; } @@ -975,6 +992,15 @@ [class*='block-compare'] { display: none; } + .catalog-product_compare-index { + .columns { + .column { + &.main { + flex-basis: inherit; + } + } + } + } } // diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less index 6bf766b7400a7..77fb53a2ab02a 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less @@ -18,7 +18,7 @@ @product-name-link__text-decoration__visited: @link__hover__text-decoration; @product-item__hover__background-color: @color-white; -@product-item__hover__box-shadow: 3px 3px 4px 0 rgba(0, 0, 0, .3); +@product-item__hover__box-shadow: 3px 4px 4px 0 rgba(0, 0, 0, .3); @product-price__muted__color: @color-gray40; @@ -34,15 +34,22 @@ .product { &-items { + .lib-inline-block-space-container(); &:extend(.abs-reset-list all); } &-item { + .lib-inline-block-space-item(); vertical-align: top; .products-grid & { display: inline-block; - width: 100%/2; + margin-left: 2%; + width: calc(~'(100% - 2%)/2'); + } + + &:nth-child(2n + 1) { + margin-left: 0; } &:extend(.abs-add-box-sizing all); @@ -68,8 +75,17 @@ } &-actions { + font-size: 0; + + > * { + font-size: 1.4rem; + } .actions-secondary { + display: inline-block; + font-size: 1.4rem; + vertical-align: middle; + > button.action { .lib-button-reset(); } @@ -79,12 +95,19 @@ &:before { margin: 0; } + line-height: 35px; + text-align: center; + width: 35px; span { &:extend(.abs-visually-hidden all); } } } + + .actions-primary { + display: inline-block; + } } &-description { @@ -291,7 +314,7 @@ border: 1px solid @color-gray-light2; border-top: none; left: 0; - margin: 9px 0 0 -1px; + margin: 10px 0 0 -1px; padding: 0 9px 9px; position: absolute; right: -1px; @@ -307,13 +330,13 @@ } .actions-primary + .actions-secondary { - display: table-cell; - padding-left: 10px; + display: inline-block; vertical-align: middle; - width: 50%; > .action { - margin-right: 10px; + line-height: 35px; + text-align: center; + width: 35px; &:last-child { margin-right: 0; @@ -322,7 +345,7 @@ } .actions-primary { - display: table-cell; + display: inline-block; } } @@ -374,10 +397,24 @@ .page-products.page-layout-3columns { .products-grid { .product-item { - width: 100%/3; + margin-left: 0; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } + + .block.widget .products-grid .product-item, + .page-layout-1column .block.widget .products-grid .product-item, + .page-layout-3columns .block.widget .products-grid .product-item { + .product-item-inner { + box-shadow: 3px 6px 4px 0 rgba(0, 0, 0, .3); + margin: 9px 0 0 -1px; + } + } } // @@ -388,7 +425,12 @@ .page-products { .products-grid { .product-item { - width: 100%/3; + margin-left: 2%; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } @@ -441,9 +483,13 @@ } .product-item { - margin-left: calc(~'(100% - 4 * 24.439%) / 3'); + margin-left: 2%; padding: 5px; - width: 24.439%; + width: calc(~'(100% - 6%)/4'); + + &:nth-child(3n + 1) { + margin-left: 2%; + } &:nth-child(4n + 1) { margin-left: 0; diff --git a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less index f785dd74d900e..31859a46d3efe 100644 --- a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less @@ -18,6 +18,20 @@ // _____________________________________________ & when (@media-common = true) { + + .search { + .fieldset { + .control { + .addon { + input { + flex-basis: auto; + width: 100%; + } + } + } + } + } + .block-search { margin-bottom: 0; @@ -199,7 +213,7 @@ // Mobile // _____________________________________________ -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .block-search { margin-top: @indent__s; } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 5aaf0cd02fab9..71814cd0f0422 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -492,6 +492,17 @@ } } } + + .cart.table-wrapper, + .order-items.table-wrapper { + .col.price, + .col.qty, + .col.subtotal, + .col.msrp { + text-align: left; + } + } + } // @@ -516,6 +527,18 @@ // Desktop // _____________________________________________ +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__s) { + .cart-container { + .block.crosssell { + .products-grid { + .product-item-actions { + margin: 0 0 @indent__s; + } + } + } + } +} + .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .checkout-cart-index { .page-main { @@ -689,6 +712,9 @@ position: static; } } + &.discount { + width: auto; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less index b9b223f44021a..5ca3322403102 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less @@ -63,6 +63,13 @@ } } + dl { + &.product.options.list { + display: inline-block; + vertical-align: top; + } + } + .text { &.empty { text-align: center; @@ -288,6 +295,15 @@ .details-qty { margin-top: @indent__s; } + + .product { + .options { + &.list { + &:extend(.abs-product-options-list all); + &:extend(.abs-add-clearfix all); + } + } + } } .product { @@ -355,7 +371,7 @@ .item-qty { margin-right: @indent__s; text-align: center; - width: 40px; + width: 45px; } .update-cart-item { @@ -400,7 +416,7 @@ } } -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .minicart-wrapper { margin-top: @indent__s; } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less index 0df0cace338c0..ac5ab0d87bf62 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less @@ -7,6 +7,8 @@ // Variables // _____________________________________________ +@import 'fields/_file-uploader.less'; + @checkout-wrapper__margin: @indent__base; @checkout-wrapper__columns: 16; @@ -48,6 +50,7 @@ .step-title { &:extend(.abs-checkout-title all); .lib-css(border-bottom, @checkout-step-title__border); + margin-bottom: 15px; } .step-content { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 9bad9518f5724..920e68994c666 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -118,6 +118,10 @@ .product { position: relative; + .item-options { + &:extend(.abs-product-options-list all); + &:extend(.abs-add-clearfix all); + } } } @@ -148,14 +152,14 @@ } .product-item-name-block { - display: table-cell; + display: block; padding-right: @indent__xs; text-align: left; } .subtotal { - display: table-cell; - text-align: right; + display: block; + text-align: left; } .price { @@ -227,3 +231,27 @@ } } } + +// +// Tablet +// _____________________________________________ + +@media only screen and (max-width: @screen__m) { + .opc-block-summary { + .product-item { + .product-item-inner { + display: block; + } + + .product-item-name-block { + display: block; + text-align: left; + } + + .subtotal { + display: block; + text-align: left; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less index 0b27454b206e3..23bb15e6fb4fe 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less @@ -69,6 +69,13 @@ .payment-option-content { .lib-css(padding, 0 0 @indent__base @checkout-payment-option-content__padding__xl); + .primary { + .action { + &.action-apply { + margin-right: 0; + } + } + } } .payment-option-inner { @@ -136,6 +143,7 @@ } } + .captcha, .number { .input-text { width: 225px; diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less index dd9db0e715308..eb9c069053661 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -63,10 +63,13 @@ } } } - + /** + * @codingStandardsIgnoreStart + */ #po_number { margin-bottom: 20px; } + // @codingStandardsIgnoreEnd } .payment-method-title { @@ -116,7 +119,8 @@ margin: 0 0 @indent__base; .primary { - .action-update { + .action-update { + margin-bottom: 20px; margin-right: 0; } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less new file mode 100644 index 0000000000000..7b06186ef9ad3 --- /dev/null +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less @@ -0,0 +1,450 @@ +// /** +// * Copyright © Magento, Inc. All rights reserved. +// * See COPYING.txt for license details. +// */ + +// +// Components -> Single File Uploader +// _____________________________________________ + +// +// Variables +// --------------------------------------------- + +@icon-delete__content: '\e604'; +@icon-file__content: '\e626'; + + +@file-uploader-preview__border-color: @color-lighter-grayish; +@file-uploader-preview__background-color: @color-white; +@file-uploader-preview-focus__color: @color-blue2; + +@file-uploader-document-icon__color: @color-gray80; +@file-uploader-document-icon__size: 7rem; +@file-uploader-document-icon__z-index: @data-grid-file-uploader-image__z-index + 1; + +@file-uploader-video-icon__color: @color-gray80; +@file-uploader-video-icon__size: 4rem; +@file-uploader-video-icon__z-index: @data-grid-file-uploader-image__z-index + 1; + +@file-uploader-placeholder-icon__color: @color-gray80; +@file-uploader-placeholder-icon__z-index: @data-grid-file-uploader-image__z-index + 1; + +@file-uploader-delete-icon__color: @color-brownie; +@file-uploader-delete-icon__hover__color: @color-brownie-vanilla; +@file-uploader-delete-icon__font-size: 1.6rem; + +@file-uploader-muted-text__color: @color-gray62; + +@file-uploader-preview__width: 150px; +@file-uploader-preview__height: @file-uploader-preview__width; +@file-uploader-preview__opacity: .7; + +@file-uploader-spinner-dimensions: 15px; + +@file-uploader-dragover__background: @color-gray83; +@file-uploader-dragover-focus__color: @color-blue2; + +// Grid uploader + +@data-grid-file-uploader-image__size: 5rem; +@data-grid-file-uploader-image__z-index: 1; + +@data-grid-file-uploader-menu-button__width: 2rem; + +@data-grid-file-uploader-upload-icon__color: @color-darkie-gray; +@data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray; +@data-grid-file-uploader-upload-icon__line-height: 48px; + +@data-grid-file-uploader-wrapper__size: @data-grid-file-uploader-image__size + 2rem; + +// +// Single file uploader +// --------------------------------------------- + +.file-uploader-area { + position: relative; + + input[type='file'] { + cursor: pointer; + opacity: 0; + overflow: hidden; + position: absolute; + visibility: hidden; + width: 0; + + &:focus { + + .file-uploader-button { + box-shadow: 0 0 0 1px @file-uploader-preview-focus__color; + } + } + + &:disabled { + + .file-uploader-button { + cursor: default; + opacity: .5; + pointer-events: none; + } + } + } +} + +.file-uploader-summary { + display: inline-block; + vertical-align: top; +} + +.file-uploader-button { + background: @color-gray-darken0; + border: 1px solid @color-gray_light; + box-sizing: border-box; + color: @color-black_dark; + cursor: pointer; + display: inline-block; + font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.4rem; + font-weight: 600; + line-height: 1.6rem; + margin: 0; + padding: 7px 15px; + vertical-align: middle; + + &._is-dragover { + background: @file-uploader-dragover__background; + border: 1px solid @file-uploader-preview-focus__color; + } +} + +.file-uploader-spinner { + background-image: url('@{baseDir}images/loader-1.gif'); + background-position: 50%; + background-repeat: no-repeat; + background-size: @file-uploader-spinner-dimensions; + display: none; + height: 30px; + margin-left: @indent__s; + vertical-align: top; + width: @file-uploader-spinner-dimensions; +} + +.file-uploader-preview { + .action-remove { + &:extend(.abs-action-reset all); + .lib-icon-font ( + @icon-delete__content, + @_icon-font: @icons__font-name, + @_icon-font-size: @file-uploader-delete-icon__font-size, + @_icon-font-color: @file-uploader-delete-icon__color, + @_icon-font-color-hover: @file-uploader-delete-icon__hover__color, + @_icon-font-text-hide: true, + @_icon-font-display: block + ); + bottom: 4px; + cursor: pointer; + display: block; + height: 27px; + left: 6px; + padding: 2px; + position: absolute; + text-decoration: none; + width: 25px; + z-index: 2; + } + + &:hover { + .preview-image img, + .preview-link:before { + opacity: @file-uploader-preview__opacity; + } + } + + .preview-link { + display: block; + height: 100%; + } + + .preview-image img { + bottom: 0; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + z-index: 1; + } + + .preview-video { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: @file-uploader-video-icon__size, + @_icon-font-color: @file-uploader-video-icon__color, + @_icon-font-color-hover: @file-uploader-video-icon__color + ); + + &:before { + left: 0; + margin-top: -@file-uploader-video-icon__size / 2; + position: absolute; + right: 0; + top: 50%; + z-index: @file-uploader-video-icon__z-index; + } + } + + .preview-document { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: @file-uploader-document-icon__size, + @_icon-font-color: @file-uploader-document-icon__color, + @_icon-font-color-hover: @file-uploader-document-icon__color + ); + + &:before { + left: 0; + margin-top: -@file-uploader-document-icon__size / 2; + position: absolute; + right: 0; + top: 50%; + z-index: @file-uploader-document-icon__z-index; + } + } +} + +.file-uploader-preview, +.file-uploader-placeholder { + background: @file-uploader-preview__background-color; + border: 1px solid @file-uploader-preview__border-color; + box-sizing: border-box; + cursor: pointer; + display: block; + height: @file-uploader-preview__height; + line-height: 1; + margin: @indent__s @indent__m @indent__s 0; + overflow: hidden; + position: relative; + width: @file-uploader-preview__width; +} + +.file-uploader { + &._loading { + .file-uploader-spinner { + display: inline-block; + } + } + + .admin__field-note, + .admin__field-error { + margin-bottom: @indent__s; + } + + .file-uploader-filename { + .lib-text-overflow(); + max-width: @file-uploader-preview__width; + word-break: break-all; + + &:first-child { + margin-bottom: @indent__s; + } + } + + .file-uploader-meta { + color: @file-uploader-muted-text__color; + } + + .admin__field-fallback-reset { + margin-left: @indent__s; + } + + ._keyfocus & .action-remove { + &:focus { + box-shadow: 0 0 0 1px @file-uploader-preview-focus__color; + } + } +} + +// Placeholder for multiple uploader +.file-uploader-placeholder { + &.placeholder-document { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 5rem, + @_icon-font-color: @file-uploader-placeholder-icon__color, + @_icon-font-color-hover: @file-uploader-placeholder-icon__color + ); + + &:before { + left: 0; + position: absolute; + right: 0; + top: 20px; + z-index: @file-uploader-placeholder-icon__z-index; + } + } + + &.placeholder-image { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 5rem, + @_icon-font-color: @file-uploader-placeholder-icon__color, + @_icon-font-color-hover: @file-uploader-placeholder-icon__color + ); + + &:before { + left: 0; + position: absolute; + right: 0; + top: 20px; + z-index: @file-uploader-placeholder-icon__z-index; + } + } + + &.placeholder-video { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 3rem, + @_icon-font-color: @file-uploader-placeholder-icon__color, + @_icon-font-color-hover: @file-uploader-placeholder-icon__color + ); + + &:before { + left: 0; + position: absolute; + right: 0; + top: 30px; + z-index: @file-uploader-placeholder-icon__z-index; + } + } +} + +.file-uploader-placeholder-text { + bottom: 0; + color: @color-blue-dodger; + font-size: 1.1rem; + left: 0; + line-height: @line-height__base; + margin-bottom: 15%; + padding: 0 @indent__base; + position: absolute; + right: 0; + text-align: center; +} + +// +// Grid image uploader +// --------------------------------------------- + +.data-grid-file-uploader { + min-width: @data-grid-file-uploader-wrapper__size; + + &._loading { + .file-uploader-spinner { + display: block; + } + + .file-uploader-button { + &:before { + display: none; + } + } + } + + .file-uploader-image { + background: transparent; + bottom: 0; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + z-index: @data-grid-file-uploader-image__z-index; + + + .file-uploader-area { + .file-uploader-button { + &:before { + display: none; + } + } + } + } + + .file-uploader-area { + z-index: @data-grid-file-uploader-image__z-index + 1; + } + + .file-uploader-spinner { + height: 100%; + margin: 0; + position: absolute; + top: 0; + width: 100%; + } + + .file-uploader-button { + display: block; + height: @data-grid-file-uploader-upload-icon__line-height; + text-align: center; + + .lib-icon-font ( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 1.3rem, + @_icon-font-line-height: @data-grid-file-uploader-upload-icon__line-height, + @_icon-font-color: @data-grid-file-uploader-upload-icon__color, + @_icon-font-color-hover: @data-grid-file-uploader-upload-icon__hover__color, + @_icon-font-text-hide: true, + @_icon-font-display: block + ); + } + + .action-select-wrap { + float: left; + + .action-select { + border: 1px solid @file-uploader-preview__border-color; + display: block; + height: @data-grid-file-uploader-image__size; + margin-left: -1px; + padding: 0; + width: @data-grid-file-uploader-menu-button__width; + + &:after { + border-color: @data-grid-file-uploader-upload-icon__color transparent transparent transparent; + left: 50%; + margin: 0 0 0 -5px; + } + + &:hover { + &:after { + border-color: @data-grid-file-uploader-upload-icon__hover__color transparent transparent transparent; + } + } + + > span { + display: none; + } + } + + .action-menu { + left: 4rem; + right: auto; + z-index: @data-grid-file-uploader-image__z-index + 1; + } + } +} + +.data-grid-file-uploader-inner { + border: 1px solid @file-uploader-preview__border-color; + float: left; + height: @data-grid-file-uploader-image__size; + position: relative; + width: @data-grid-file-uploader-image__size; +} diff --git a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index 5418d836fc262..6adf4b5b2f86b 100755 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -161,6 +161,7 @@ .table-wrapper { .lib-css(margin-bottom, @indent__base); border-bottom: 1px solid @account-table-border-bottom-color; + overflow-x: auto; &:last-child { margin-bottom: 0; @@ -199,7 +200,7 @@ // Checkout address (create shipping address) .field.street { - .field.additional { + .field { .label { &:extend(.abs-visually-hidden all); } @@ -334,13 +335,17 @@ } } - .order-products-toolbar { + .order-products-toolbar, + .customer-addresses-toolbar { position: relative; .toolbar-amount { position: relative; text-align: center; } + .pages { + position: relative; + } } } @@ -371,7 +376,7 @@ .fieldset { > .field { > .control { - width: 55%; + width: 80%; } } } @@ -402,7 +407,9 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.search.advanced, + .form.form-orders-search { min-width: 600px; width: 50%; } @@ -417,6 +424,12 @@ .column.main { width: 77.7%; } + + .sidebar-main { + .block { + margin-bottom: 0; + } + } } .account { @@ -528,11 +541,18 @@ .column.main, .sidebar-additional { margin: 0; + padding: 0; } .data.table { &:extend(.abs-table-striped-mobile all); } + + .sidebar-main { + .account-nav { + margin-bottom: 0; + } + } } } @@ -550,8 +570,8 @@ } .account { - .page.messages { - margin-bottom: @indent__base; + .messages { + margin-bottom: 0; } .column.main { @@ -589,4 +609,15 @@ position: relative; } } + + .form.search.advanced { + .field.price { + .with-addon { + .input-text { + flex-basis: auto; + width: 100%; + } + } + } + } } diff --git a/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less index 41e6da39e1ef1..ff377a4b88acc 100644 --- a/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less @@ -246,6 +246,9 @@ .gift-messages-order { margin-bottom: @indent__m; } + .gift-message-summary { + padding-right: 7rem; + } } // @@ -282,10 +285,6 @@ } } - .gift-message-summary { - padding-right: 7rem; - } - // // In-table block // --------------------------------------------- diff --git a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less index 088372808aa6a..e9c40f6386246 100644 --- a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less @@ -70,6 +70,10 @@ clear: left; } } + + .box-tocart { + margin-top: @indent__s; + } } } @@ -133,9 +137,18 @@ } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { - .table-wrapper.grouped { - .lib-css(margin-left, -@layout__width-xs-indent); - .lib-css(margin-right, -@layout__width-xs-indent); + .product-add-form { + .table-wrapper.grouped { + .lib-css(margin-left, -@layout__width-xs-indent); + .lib-css(margin-right, -@layout__width-xs-indent); + .table.data.grouped { + tr { + td { + padding: 5px 10px 5px 15px; + } + } + } + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less index 112184b45fe86..475361c56afc8 100644 --- a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less @@ -74,6 +74,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price, .product-item .map-old-price, .product-info-price .map-show-info { diff --git a/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less index 0b01c54a64378..7ed4a9e64e943 100644 --- a/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less @@ -429,7 +429,7 @@ .product { &-item { &-checkbox { - left: 20px; + left: 0; position: absolute; top: 20px; } diff --git a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less index ed6b53727da52..a94e2cae46b14 100644 --- a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less @@ -345,6 +345,22 @@ .data.table { &:extend(.abs-checkout-order-review all); + &.table-order-review { + > tbody { + > tr { + > td { + &.col { + &.subtotal { + border-bottom: none; + } + &.qty { + text-align: center; + } + } + } + } + } + } } } @@ -374,7 +390,7 @@ text-align: right; .action { - margin-left: @indent__s; + margin-left: 0; &.back { display: block; @@ -496,4 +512,12 @@ margin-left: @indent__xl; } } + + .multicheckout { + .actions-toolbar { + > .primary { + margin-right: 0; + } + } + } } diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index 9ccd6c190ec0e..d7ee1319c9a43 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -81,3 +81,24 @@ width: 34%; } } + +// +// Mobile +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .block { + &.newsletter { + input { + font-size: 12px; + padding-left: 30px; + } + + .field { + .control:before { + font-size: 13px; + } + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less index 2c66420f65fbd..4b5e03f8013b0 100644 --- a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less @@ -297,6 +297,9 @@ a:not(:last-child) { margin-right: 30px; } + .action.add { + white-space: nowrap; + } } } @@ -360,6 +363,7 @@ .label { font-weight: @font-weight__semibold; margin-right: @indent__s; + vertical-align: middle; } } } diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html index a7b9b330ab9ce..269e46d752084 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html @@ -11,7 +11,7 @@ "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var order.getCustomerName()":"Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html index 36279eb26005e..c8bdae7b08fa5 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html @@ -10,7 +10,7 @@ "var creditmemo.increment_id":"Credit Memo Id", "var billing.getName()":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html index a739c9f54b08f..8ec54f1e64d9c 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html @@ -11,7 +11,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html index a56ee6da9fa25..6028db7b97730 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html @@ -10,7 +10,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html index 3e4bf8df2f107..fa16ac2196bf4 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html index 1075608db4341..8ead615fe01ca 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -22,7 +22,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html index 8c4084fcaf496..e467aa843e2f4 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html @@ -51,7 +51,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html index 68f1886986c5b..385110f8f037e 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html @@ -49,7 +49,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html index 37bf92b866c74..4f9b7286f3ae4 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html index 954819949860b..3ef26463ea755 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less index 3f19d1020bab9..31c128e07e3a6 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less @@ -76,8 +76,10 @@ } // Remove address and phone number link color on iOS -.address-details a { - &:extend(.no-link a); +.email-non-inline() { + .address-details a { + &:extend(.no-link a); + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__xs) { diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index 1e4a92fa0701f..1be46c8239ee2 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less @@ -343,16 +343,22 @@ } .product-item-name { - display: inline-block; + float: left; + width: calc(100% - 20px); + } + .product-item::after { + clear: both; + content: ''; + display: table; } - .product-item { .label { &:extend(.abs-visually-hidden all); } .field.item { - display: inline-block; + float: left; + width: 20px; } } } @@ -392,6 +398,17 @@ &.orders-recent { &:extend(.abs-account-table-margin-mobile all); &:extend(.abs-no-border-top all); + .table-order-items { + &.table { + tbody { + > tr { + > td.col { + padding-left: 0; + } + } + } + } + } } } @@ -555,13 +572,13 @@ margin: 0 @tab-control__margin-right 0 0; a { - padding: @tab-control__padding-top @tab-control__padding-right; + padding: @tab-control__padding-top @indent__base; } strong { border-bottom: 0; margin-bottom: -1px; - padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + padding: @tab-control__padding-top @indent__base @tab-control__padding-bottom + 1 @indent__base; } } } @@ -687,3 +704,19 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__l) { + .order-links { + .item { + margin: 0 @tab-control__margin-right 0 0; + + a { + padding: @tab-control__padding-top @tab-control__padding-right; + } + + strong { + padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less index baf5468b18485..3435736a54a6a 100644 --- a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less @@ -10,6 +10,14 @@ & when (@media-common = true) { .form.send.friend { &:extend(.abs-add-fields all); + + .fieldset { + .field { + .control { + width: 100%; + } + } + } } .product-social-links .action.mailto.friend { @@ -44,3 +52,18 @@ } } } +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .form.send.friend { + .fieldset { + padding-bottom: @indent__xs; + } + + .action { + &.remove { + margin-left: 0; + right: 0; + top: 100%; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml b/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml index 2dfb1d3ee6bf0..7e64e5f3f01cd 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml +++ b/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml @@ -7,6 +7,6 @@ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <head> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> </head> </page> diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index cadf575b95fc7..dfcc51e0a0a26 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less @@ -77,6 +77,14 @@ .lib-vendor-prefix-flex-grow(1); } + .page-main { + > .page-title-wrapper { + .page-title { + word-break: break-all; + } + } + } + // // Header // --------------------------------------------- @@ -104,6 +112,10 @@ font-size: @font-size__base; margin: 0 0 0 15px; + &.customer-welcome { + margin: 0 0 0 5px; + } + > a { .lib-link( @_link-color: @header-panel__text-color, @@ -144,6 +156,12 @@ } } + .page-print { + .nav-toggle { + display: none; + } + } + .page-main { > .page-title-wrapper { .page-title + .action { @@ -312,6 +330,23 @@ } } } + .page-header { + .switcher { + .options { + ul.dropdown { + right: 0; + &:before { + left: auto; + right: 10px; + } + &:after { + left: auto; + right: 9px; + } + } + } + } + } // // Widgets @@ -422,7 +457,7 @@ } } -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .logo { margin-bottom: 13px; margin-top: 4px; @@ -435,7 +470,7 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .cms-page-view .page-main { - padding-top: 41px; + padding-top: 0; position: relative; } } diff --git a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less index 584eefb9bc643..85e8aeb0b515c 100644 --- a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less @@ -8,6 +8,24 @@ // _____________________________________________ & when (@media-common = true) { + .toolbar { + &.wishlist-toolbar { + .limiter { + float: right; + } + .main .pages { + display: inline-block; + position: relative; + z-index: 0; + } + .toolbar-amount, + .limiter { + display: inline-block; + z-index: 1; + } + } + } + .form.wishlist.items { .actions-toolbar { &:extend(.abs-reset-left-margin all); @@ -164,6 +182,30 @@ } } } + .products-grid.wishlist { + .product-item-actions { + .action { + &.edit, + &.delete { + .lib-icon-font( + @icon-edit, + @_icon-font-size: 18px, + @_icon-font-line-height: 20px, + @_icon-font-text-hide: true, + @_icon-font-color: @minicart-icons-color, + @_icon-font-color-hover: @primary__color, + @_icon-font-color-active: @minicart-icons-color + ); + } + + &.delete { + .lib-icon-font-symbol( + @_icon-font-content: @icon-trash + ); + } + } + } + } } // @@ -185,11 +227,11 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .products-grid.wishlist { margin-bottom: @indent__l; - margin-right: -@indent__s; + margin-right: 0; .product { &-item { - padding: @indent__base @indent__s @indent__base @indent__base; + padding: @indent__base 0 @indent__base 0; position: relative; &-photo { @@ -203,6 +245,7 @@ &-actions { display: block; + float: left; .action { margin-right: 15px; @@ -210,15 +253,7 @@ &:last-child { margin-right: 0; } - - &.edit { - float: left; - } - - &.delete { - float: right; - } - + &.edit, &.delete { margin-top: 7px; @@ -360,9 +395,7 @@ width: auto; } } -} -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .wishlist-index-index { .product-item-inner { @_shadow: 3px 4px 4px 0 rgba(0, 0, 0, .3); diff --git a/app/design/frontend/Magento/luma/etc/view.xml b/app/design/frontend/Magento/luma/etc/view.xml index 55d43272caad9..7aa2e51481bd9 100644 --- a/app/design/frontend/Magento/luma/etc/view.xml +++ b/app/design/frontend/Magento/luma/etc/view.xml @@ -256,7 +256,7 @@ <var name="product_list_image_size">166</var> <!-- New Product image size used in product list --> <var name="product_zoom_image_size">370</var> <!-- New Product image size used for zooming --> - <var name="product_image_white_borders">0</var> + <var name="product_image_white_borders">1</var> </vars> <vars module="Magento_Bundle"> <var name="product_summary_image_size">58</var> <!-- New Product image size used for summary block--> diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less index 0c7150c18550b..8533318a12d1b 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_forms.less +++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less @@ -20,7 +20,7 @@ .lib-form-fieldset(); &:last-child { - margin-bottom: 0; + margin-bottom: @indent__base; } > .field, @@ -104,6 +104,10 @@ .select-styling(); } + select.admin__control-multiselect { + height: auto; + } + .field-error, div.mage-error[generated] { margin-top: 7px; @@ -113,6 +117,18 @@ .lib-form-validation-note(); } + .product-options-wrapper { + .date { + &.required { + div[for*='options'] { + &.mage-error { + display: none !important; + } + } + } + } + } + // TEMP .field .tooltip { diff --git a/app/design/frontend/Magento/luma/web/css/source/_sections.less b/app/design/frontend/Magento/luma/web/css/source/_sections.less index 73665fd22da23..95769c4f4b6ba 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_sections.less +++ b/app/design/frontend/Magento/luma/web/css/source/_sections.less @@ -19,16 +19,16 @@ a { position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: @font-size__base, - @_icon-font-line-height: @icon-font__line-height, - @_icon-font-color: @icon-font__color, - @_icon-font-color-hover: @icon-font__color-hover, - @_icon-font-color-active: @icon-font__color-active, - @_icon-font-margin: @icon-font__margin, - @_icon-font-vertical-align: @icon-font__vertical-align, - @_icon-font-position: after, - @_icon-font-display: false + @_icon-font-content: @icon-down, + @_icon-font-size: @font-size__base, + @_icon-font-line-height: @icon-font__line-height, + @_icon-font-color: @icon-font__color, + @_icon-font-color-hover: @icon-font__color-hover, + @_icon-font-color-active: @icon-font__color-active, + @_icon-font-margin: @icon-font__margin, + @_icon-font-vertical-align: @icon-font__vertical-align, + @_icon-font-position: after, + @_icon-font-display: false ); &:after { @@ -75,3 +75,17 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .product.data.items { + .item.title { + > .switch { + padding: 1px 15px 1px; + } + } + + > .item.content { + padding: 10px 15px 30px; + } + } +} diff --git a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less index 48a8b0bc6b591..441b6669b545f 100644 --- a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less @@ -58,7 +58,7 @@ .modal-custom { .action-close { - .lib-css(margin, @indent__m); + .lib-css(margin, 15px); } } diff --git a/app/etc/di.xml b/app/etc/di.xml index 6cf169c1d2277..46903c99e422c 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -38,7 +38,7 @@ <preference for="Magento\Framework\Locale\ListsInterface" type="Magento\Framework\Locale\TranslatedLists" /> <preference for="Magento\Framework\Locale\AvailableLocalesInterface" type="Magento\Framework\Locale\Deployed\Codes" /> <preference for="Magento\Framework\Locale\OptionInterface" type="Magento\Framework\Locale\Deployed\Options" /> - <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Backend\Database" /> + <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Proxy" /> <preference for="Magento\Framework\Api\AttributeTypeResolverInterface" type="Magento\Framework\Reflection\AttributeTypeResolver" /> <preference for="Magento\Framework\Api\Search\SearchResultInterface" type="Magento\Framework\Api\Search\SearchResult" /> <preference for="Magento\Framework\Api\Search\SearchCriteriaInterface" type="Magento\Framework\Api\Search\SearchCriteria"/> @@ -209,6 +209,7 @@ <preference for="Magento\Framework\MessageQueue\Bulk\ExchangeFactoryInterface" type="Magento\Framework\MessageQueue\Bulk\ExchangeFactory" /> <preference for="Magento\Framework\MessageQueue\QueueFactoryInterface" type="Magento\Framework\MessageQueue\QueueFactory" /> <preference for="Magento\Framework\Search\Request\IndexScopeResolverInterface" type="Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver"/> + <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> @@ -1738,7 +1739,7 @@ <argument name="map" xsi:type="array"> <item name="OPTIONS" xsi:type="string">\Magento\Framework\App\Action\HttpOptionsActionInterface</item> <item name="GET" xsi:type="string">\Magento\Framework\App\Action\HttpGetActionInterface</item> - <item name="HEAD" xsi:type="string">\Magento\Framework\App\Action\HttpHeadActionInterface</item> + <item name="HEAD" xsi:type="string">\Magento\Framework\App\Action\HttpGetActionInterface</item> <item name="POST" xsi:type="string">\Magento\Framework\App\Action\HttpPostActionInterface</item> <item name="PUT" xsi:type="string">\Magento\Framework\App\Action\HttpPutActionInterface</item> <item name="PATCH" xsi:type="string">\Magento\Framework\App\Action\HttpPatchActionInterface</item> @@ -1756,4 +1757,11 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockTimeout" xsi:type="number">10000</argument> + <argument name="delayTimeout" xsi:type="number">20</argument> + </arguments> + </type> </config> diff --git a/auth.json.sample b/auth.json.sample index 81c8fd220eae2..be1c70cfe1e18 100644 --- a/auth.json.sample +++ b/auth.json.sample @@ -1,8 +1,8 @@ { - "http-basic": { - "repo.magento.com": { - "username": "<public-key>", - "password": "<private-key>" - } - } + "http-basic": { + "repo.magento.com": { + "username": "<public-key>", + "password": "<private-key>" + } + } } diff --git a/composer.json b/composer.json index 62b3c95135669..525f3a21d9577 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "colinmollenhour/credis": "1.10.0", "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", - "elasticsearch/elasticsearch": "~2.0|~5.1", + "elasticsearch/elasticsearch": "~2.0|~5.1|~6.1", "magento/composer": "~1.4.0", "magento/magento-composer-installer": ">=0.1.11", "magento/zendframework1": "~1.14.1", @@ -82,15 +82,16 @@ "zendframework/zend-view": "~2.10.0" }, "require-dev": { + "allure-framework/allure-phpunit": "~1.2.0", "friendsofphp/php-cs-fixer": "~2.13.0", "lusitanian/oauth": "~0.8.10", - "magento/magento2-functional-testing-framework": "~2.3.12", + "magento/magento-coding-standard": "~1.0.0", + "magento/magento2-functional-testing-framework": "~2.3.14", "pdepend/pdepend": "2.5.2", "phpmd/phpmd": "@stable", "phpunit/phpunit": "~6.5.0", "sebastian/phpcpd": "~3.0.0", - "squizlabs/php_codesniffer": "3.3.1", - "allure-framework/allure-phpunit": "~1.2.0" + "squizlabs/php_codesniffer": "3.3.1" }, "suggest": { "ext-pcntl": "Need for run processes in parallel mode" @@ -104,6 +105,7 @@ "magento/module-asynchronous-operations": "*", "magento/module-authorization": "*", "magento/module-authorizenet": "*", + "magento/module-authorizenet-acceptjs": "*", "magento/module-advanced-search": "*", "magento/module-backend": "*", "magento/module-backup": "*", @@ -142,11 +144,13 @@ "magento/module-developer": "*", "magento/module-dhl": "*", "magento/module-directory": "*", + "magento/module-directory-graph-ql": "*", "magento/module-downloadable": "*", "magento/module-downloadable-graph-ql": "*", "magento/module-downloadable-import-export": "*", "magento/module-eav": "*", "magento/module-elasticsearch": "*", + "magento/module-elasticsearch-6": "*", "magento/module-email": "*", "magento/module-encryption-key": "*", "magento/module-fedex": "*", @@ -178,6 +182,8 @@ "magento/module-media-storage": "*", "magento/module-message-queue": "*", "magento/module-msrp": "*", + "magento/module-msrp-configurable-product": "*", + "magento/module-msrp-grouped-product": "*", "magento/module-multishipping": "*", "magento/module-mysql-mq": "*", "magento/module-new-relic-reporting": "*", @@ -187,6 +193,7 @@ "magento/module-page-cache": "*", "magento/module-payment": "*", "magento/module-paypal": "*", + "magento/module-paypal-captcha": "*", "magento/module-persistent": "*", "magento/module-product-alert": "*", "magento/module-product-video": "*", @@ -234,6 +241,7 @@ "magento/module-usps": "*", "magento/module-variable": "*", "magento/module-vault": "*", + "magento/module-vault-graph-ql": "*", "magento/module-version": "*", "magento/module-webapi": "*", "magento/module-webapi-async": "*", diff --git a/composer.lock b/composer.lock index d24dfd2b612fc..06ab71ee75970 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": "e2fcf8723503311ee9fea99dece55225", + "content-hash": "c43d19692d25afef14dd42eb893eb4ca", "packages": [ { "name": "braintree/braintree_php", @@ -164,21 +164,21 @@ }, { "name": "colinmollenhour/php-redis-session-abstract", - "version": "v1.4.0", + "version": "v1.4.1", "source": { "type": "git", "url": "https://github.com/colinmollenhour/php-redis-session-abstract.git", - "reference": "4cb15d557f58f45ad257cbcce3c12511e6deb5bc" + "reference": "4949ca28b86037abb44984c77bab9d0a4e075643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/4cb15d557f58f45ad257cbcce3c12511e6deb5bc", - "reference": "4cb15d557f58f45ad257cbcce3c12511e6deb5bc", + "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/4949ca28b86037abb44984c77bab9d0a4e075643", + "reference": "4949ca28b86037abb44984c77bab9d0a4e075643", "shasum": "" }, "require": { "colinmollenhour/credis": "~1.6", - "php": "~5.5.0|~5.6.0|~7.0.0|~7.1.0|~7.2.0" + "php": "^5.5 || ^7.0" }, "type": "library", "autoload": { @@ -197,20 +197,20 @@ ], "description": "A Redis-based session handler with optimistic locking", "homepage": "https://github.com/colinmollenhour/php-redis-session-abstract", - "time": "2018-03-29T15:54:15+00:00" + "time": "2019-03-18T14:43:14+00:00" }, { "name": "composer/ca-bundle", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660" + "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660", - "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/558f321c52faeb4828c03e7dc0cfe39a09e09a2d", + "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d", "shasum": "" }, "require": { @@ -253,20 +253,20 @@ "ssl", "tls" ], - "time": "2018-10-18T06:09:13+00:00" + "time": "2019-01-28T09:30:10+00:00" }, { "name": "composer/composer", - "version": "1.8.0", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "d8aef3af866b28786ce9b8647e52c42496436669" + "reference": "bc364c2480c17941e2135cfc568fa41794392534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669", - "reference": "d8aef3af866b28786ce9b8647e52c42496436669", + "url": "https://api.github.com/repos/composer/composer/zipball/bc364c2480c17941e2135cfc568fa41794392534", + "reference": "bc364c2480c17941e2135cfc568fa41794392534", "shasum": "" }, "require": { @@ -333,20 +333,20 @@ "dependency", "package" ], - "time": "2018-12-03T09:31:16+00:00" + "time": "2019-02-11T09:52:10+00:00" }, { "name": "composer/semver", - "version": "1.4.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e", "shasum": "" }, "require": { @@ -395,28 +395,27 @@ "validation", "versioning" ], - "time": "2016-08-30T16:08:34+00:00" + "time": "2019-03-19T17:25:45+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2" + "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2", - "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d", + "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7" }, "type": "library", "extra": { @@ -456,20 +455,20 @@ "spdx", "validator" ], - "time": "2018-11-01T09:45:54+00:00" + "time": "2019-03-26T10:23:26+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.3.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "dc523135366eb68f22268d069ea7749486458562" + "reference": "d17708133b6c276d6e42ef887a877866b909d892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562", - "reference": "dc523135366eb68f22268d069ea7749486458562", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/d17708133b6c276d6e42ef887a877866b909d892", + "reference": "d17708133b6c276d6e42ef887a877866b909d892", "shasum": "" }, "require": { @@ -500,7 +499,7 @@ "Xdebug", "performance" ], - "time": "2018-11-29T10:59:02+00:00" + "time": "2019-01-28T20:25:53+00:00" }, { "name": "container-interop/container-interop", @@ -535,29 +534,31 @@ }, { "name": "elasticsearch/elasticsearch", - "version": "v5.3.2", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/elastic/elasticsearch-php.git", - "reference": "4b29a4121e790bbfe690d5ee77da348b62d48eb8" + "reference": "b237a37b2cdf23a5a17fd3576cdea771394ad00d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/4b29a4121e790bbfe690d5ee77da348b62d48eb8", - "reference": "4b29a4121e790bbfe690d5ee77da348b62d48eb8", + "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/b237a37b2cdf23a5a17fd3576cdea771394ad00d", + "reference": "b237a37b2cdf23a5a17fd3576cdea771394ad00d", "shasum": "" }, "require": { + "ext-json": ">=1.3.7", "guzzlehttp/ringphp": "~1.0", - "php": "^5.6|^7.0", + "php": "^7.0", "psr/log": "~1.0" }, "require-dev": { "cpliakas/git-wrapper": "~1.0", "doctrine/inflector": "^1.1", "mockery/mockery": "0.9.4", - "phpunit/phpunit": "^4.7|^5.4", - "sami/sami": "~3.2", + "phpstan/phpstan-shim": "0.8.3", + "phpunit/phpunit": "6.3.0", + "squizlabs/php_codesniffer": "3.0.2", "symfony/finder": "^2.8", "symfony/yaml": "^2.8" }, @@ -586,7 +587,7 @@ "elasticsearch", "search" ], - "time": "2017-11-08T17:04:47+00:00" + "time": "2019-01-08T18:53:46+00:00" }, { "name": "guzzlehttp/ringphp", @@ -691,23 +692,23 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.7", + "version": "5.2.8", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "8560d4314577199ba51bf2032f02cd1315587c23" + "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23", - "reference": "8560d4314577199ba51bf2032f02cd1315587c23", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4", + "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.1", + "friendsofphp/php-cs-fixer": "~2.2.20", "json-schema/json-schema-test-suite": "1.2.0", "phpunit/phpunit": "^4.8.35" }, @@ -753,7 +754,7 @@ "json", "schema" ], - "time": "2018-02-14T22:26:30+00:00" + "time": "2019-01-14T23:55:14+00:00" }, { "name": "magento/composer", @@ -1104,21 +1105,21 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.8.0", + "version": "v1.9.1", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "5e280b50cdaf8da4cc4810e0847a9618be29703d" + "reference": "87125d5b265f98c4d1b8d83a1f0726607c229421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/5e280b50cdaf8da4cc4810e0847a9618be29703d", - "reference": "5e280b50cdaf8da4cc4810e0847a9618be29703d", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/87125d5b265f98c4d1b8d83a1f0726607c229421", + "reference": "87125d5b265f98c4d1b8d83a1f0726607c229421", "shasum": "" }, "require": { "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7" + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" }, "require-dev": { "phpunit/phpunit": "^3|^4|^5" @@ -1182,7 +1183,7 @@ "secret-key cryptography", "side-channel resistant" ], - "time": "2018-11-29T22:33:39+00:00" + "time": "2019-03-20T17:19:05+00:00" }, { "name": "pelago/emogrifier", @@ -1380,16 +1381,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.12", + "version": "2.0.15", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "8814dc7841db159daed0b32c2b08fb7e03c6afe7" + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/8814dc7841db159daed0b32c2b08fb7e03c6afe7", - "reference": "8814dc7841db159daed0b32c2b08fb7e03c6afe7", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/11cf67cf78dc4acb18dc9149a57be4aee5036ce0", + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0", "shasum": "" }, "require": { @@ -1468,7 +1469,7 @@ "x.509", "x509" ], - "time": "2018-11-04T05:45:48+00:00" + "time": "2019-03-10T16:53:45+00:00" }, { "name": "psr/container", @@ -1700,16 +1701,16 @@ }, { "name": "react/promise", - "version": "v2.7.0", + "version": "v2.7.1", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f4edc2581617431aea50430749db55cc3fc031b3" + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f4edc2581617431aea50430749db55cc3fc031b3", - "reference": "f4edc2581617431aea50430749db55cc3fc031b3", + "url": "https://api.github.com/repos/reactphp/promise/zipball/31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d", "shasum": "" }, "require": { @@ -1742,7 +1743,7 @@ "promise", "promises" ], - "time": "2018-06-13T15:59:06+00:00" + "time": "2019-01-07T21:25:54+00:00" }, { "name": "seld/jsonlint", @@ -1839,16 +1840,16 @@ }, { "name": "symfony/console", - "version": "v4.1.9", + "version": "v4.1.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c74f4d1988dfcd8760273e53551694da32b056d0" + "reference": "9e87c798f67dc9fceeb4f3d57847b52d945d1a02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c74f4d1988dfcd8760273e53551694da32b056d0", - "reference": "c74f4d1988dfcd8760273e53551694da32b056d0", + "url": "https://api.github.com/repos/symfony/console/zipball/9e87c798f67dc9fceeb4f3d57847b52d945d1a02", + "reference": "9e87c798f67dc9fceeb4f3d57847b52d945d1a02", "shasum": "" }, "require": { @@ -1859,6 +1860,9 @@ "symfony/dependency-injection": "<3.4", "symfony/process": "<3.3" }, + "provide": { + "psr/log-implementation": "1.0" + }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~3.4|~4.0", @@ -1868,7 +1872,7 @@ "symfony/process": "~3.4|~4.0" }, "suggest": { - "psr/log-implementation": "For using the console logger", + "psr/log": "For using the console logger", "symfony/event-dispatcher": "", "symfony/lock": "", "symfony/process": "" @@ -1903,20 +1907,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-11-26T14:00:40+00:00" + "time": "2019-01-25T14:34:37+00:00" }, { "name": "symfony/css-selector", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "aa9fa526ba1b2ec087ffdfb32753803d999fcfcd" + "reference": "48eddf66950fa57996e1be4a55916d65c10c604a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/aa9fa526ba1b2ec087ffdfb32753803d999fcfcd", - "reference": "aa9fa526ba1b2ec087ffdfb32753803d999fcfcd", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/48eddf66950fa57996e1be4a55916d65c10c604a", + "reference": "48eddf66950fa57996e1be4a55916d65c10c604a", "shasum": "" }, "require": { @@ -1956,20 +1960,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-11-11T19:52:12+00:00" + "time": "2019-01-16T20:31:39+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.1.9", + "version": "v4.1.11", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "c4a3b5d70c05e5e7de4f22a3e840cdb173ccd7bf" + "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c4a3b5d70c05e5e7de4f22a3e840cdb173ccd7bf", - "reference": "c4a3b5d70c05e5e7de4f22a3e840cdb173ccd7bf", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51be1b61dfe04d64a260223f2b81475fa8066b97", + "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97", "shasum": "" }, "require": { @@ -2019,20 +2023,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-12-01T08:51:37+00:00" + "time": "2019-01-16T18:35:49+00:00" }, { "name": "symfony/filesystem", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710" + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/2f4c8b999b3b7cadb2a69390b01af70886753710", - "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601", + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601", "shasum": "" }, "require": { @@ -2069,20 +2073,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-11-11T19:52:12+00:00" + "time": "2019-02-07T11:40:08+00:00" }, { "name": "symfony/finder", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e53d477d7b5c4982d0e1bfd2298dbee63d01441d" + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e53d477d7b5c4982d0e1bfd2298dbee63d01441d", - "reference": "e53d477d7b5c4982d0e1bfd2298dbee63d01441d", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", "shasum": "" }, "require": { @@ -2118,20 +2122,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-11-11T19:52:12+00:00" + "time": "2019-02-23T15:42:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + "reference": "82ebae02209c21113908c229e9883c419720738a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", + "reference": "82ebae02209c21113908c229e9883c419720738a", "shasum": "" }, "require": { @@ -2143,7 +2147,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -2165,7 +2169,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -2176,20 +2180,20 @@ "polyfill", "portable" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", - "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", "shasum": "" }, "require": { @@ -2201,7 +2205,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -2235,20 +2239,20 @@ "portable", "shim" ], - "time": "2018-09-21T13:07:52+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/process", - "version": "v4.1.9", + "version": "v4.1.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "471f6e24172366a97365baaae588ddaafbba9b20" + "reference": "72d838aafaa7c790330fe362b9cecec362c64629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/471f6e24172366a97365baaae588ddaafbba9b20", - "reference": "471f6e24172366a97365baaae588ddaafbba9b20", + "url": "https://api.github.com/repos/symfony/process/zipball/72d838aafaa7c790330fe362b9cecec362c64629", + "reference": "72d838aafaa7c790330fe362b9cecec362c64629", "shasum": "" }, "require": { @@ -2284,7 +2288,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-11-20T16:14:00+00:00" + "time": "2019-01-16T19:07:26+00:00" }, { "name": "tedivm/jshrink", @@ -2748,16 +2752,16 @@ }, { "name": "zendframework/zend-db", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-db.git", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9" + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-db/zipball/5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", + "url": "https://api.github.com/repos/zendframework/zend-db/zipball/77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", "shasum": "" }, "require": { @@ -2768,7 +2772,7 @@ "phpunit/phpunit": "^5.7.25 || ^6.4.4", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { @@ -2802,7 +2806,7 @@ "db", "zf" ], - "time": "2018-04-09T13:21:36+00:00" + "time": "2019-02-25T11:37:45+00:00" }, { "name": "zendframework/zend-di", @@ -3070,16 +3074,16 @@ }, { "name": "zendframework/zend-filter", - "version": "2.9.0", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "875da9790e5cb16b9a12f41453d5f7c441452daf" + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/875da9790e5cb16b9a12f41453d5f7c441452daf", - "reference": "875da9790e5cb16b9a12f41453d5f7c441452daf", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", "shasum": "" }, "require": { @@ -3131,7 +3135,7 @@ "filter", "zf" ], - "time": "2018-12-12T23:14:25+00:00" + "time": "2018-12-17T16:00:04+00:00" }, { "name": "zendframework/zend-form", @@ -3213,16 +3217,16 @@ }, { "name": "zendframework/zend-http", - "version": "2.8.2", + "version": "2.8.4", "source": { "type": "git", "url": "https://github.com/zendframework/zend-http.git", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b" + "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-http/zipball/2c8aed3d25522618573194e7cc51351f8cd4a45b", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b", + "url": "https://api.github.com/repos/zendframework/zend-http/zipball/d160aedc096be230af0fe9c31151b2b33ad4e807", + "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807", "shasum": "" }, "require": { @@ -3264,7 +3268,7 @@ "zend", "zf" ], - "time": "2018-08-13T18:47:03+00:00" + "time": "2019-02-07T17:47:08+00:00" }, { "name": "zendframework/zend-hydrator", @@ -3394,34 +3398,38 @@ }, { "name": "zendframework/zend-inputfilter", - "version": "2.8.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-inputfilter.git", - "reference": "799ad48ed1666d3c62126fec73dd20453b3a9e4d" + "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/799ad48ed1666d3c62126fec73dd20453b3a9e4d", - "reference": "799ad48ed1666d3c62126fec73dd20453b3a9e4d", + "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", + "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", - "zendframework/zend-filter": "^2.6", + "zendframework/zend-filter": "^2.9.1", "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1", "zendframework/zend-stdlib": "^2.7 || ^3.0", - "zendframework/zend-validator": "^2.10.1" + "zendframework/zend-validator": "^2.11" }, "require-dev": { "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-message": "^1.0", "zendframework/zend-coding-standard": "~1.0.0" }, + "suggest": { + "psr/http-message-implementation": "PSR-7 is required if you wish to validate PSR-7 UploadedFileInterface payloads" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" }, "zf": { "component": "Zend\\InputFilter", @@ -3443,7 +3451,7 @@ "inputfilter", "zf" ], - "time": "2018-12-13T22:51:54+00:00" + "time": "2019-01-30T16:58:51+00:00" }, { "name": "zendframework/zend-json", @@ -4368,16 +4376,16 @@ }, { "name": "zendframework/zend-uri", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f" + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/3b6463645c6766f78ce537c70cb4fdabee1e725f", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/b2785cd38fe379a784645449db86f21b7739b1ee", + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee", "shasum": "" }, "require": { @@ -4392,8 +4400,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev", - "dev-develop": "2.7.x-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "autoload": { @@ -4411,20 +4419,20 @@ "uri", "zf" ], - "time": "2018-04-30T13:40:08+00:00" + "time": "2019-02-27T21:39:04+00:00" }, { "name": "zendframework/zend-validator", - "version": "2.11.0", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "f0789b4c4c099afdd2ecc58cc209a26c64bd4f17" + "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/f0789b4c4c099afdd2ecc58cc209a26c64bd4f17", - "reference": "f0789b4c4c099afdd2ecc58cc209a26c64bd4f17", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/3c28dfe4e5951ba38059cea895244d9d206190b3", + "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3", "shasum": "" }, "require": { @@ -4484,7 +4492,7 @@ "validator", "zf2" ], - "time": "2018-12-13T21:23:15+00:00" + "time": "2019-01-29T22:26:39+00:00" }, { "name": "zendframework/zend-view", @@ -4730,16 +4738,16 @@ }, { "name": "behat/gherkin", - "version": "v4.4.5", + "version": "v4.6.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07", + "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07", "shasum": "" }, "require": { @@ -4747,8 +4755,8 @@ }, "require-dev": { "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3", - "symfony/yaml": "~2.3|~3" + "symfony/phpunit-bridge": "~2.7|~3|~4", + "symfony/yaml": "~2.3|~3|~4" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -4785,35 +4793,32 @@ "gherkin", "parser" ], - "time": "2016-10-30T11:50:56+00:00" + "time": "2019-01-16T14:22:17+00:00" }, { "name": "codeception/codeception", - "version": "2.3.9", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fee32d5c82791548931cbc34806b4de6aa1abfc", + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc", "shasum": "" }, "require": { - "behat/gherkin": "~4.4.0", - "codeception/stub": "^1.0", + "behat/gherkin": "^4.4.0", + "codeception/phpunit-wrapper": "^6.0.9|^7.0.6", + "codeception/stub": "^2.0", "ext-json": "*", "ext-mbstring": "*", "facebook/webdriver": ">=1.1.3 <2.0", "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", - "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <3.0", + "php": ">=5.6.0 <8.0", "symfony/browser-kit": ">=2.7 <5.0", "symfony/console": ">=2.7 <5.0", "symfony/css-selector": ">=2.7 <5.0", @@ -4879,27 +4884,70 @@ "functional testing", "unit testing" ], - "time": "2018-02-26T23:29:41+00:00" + "time": "2018-08-01T07:21:49+00:00" }, { - "name": "codeception/stub", - "version": "1.0.4", + "name": "codeception/phpunit-wrapper", + "version": "6.6.1", "source": { "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805" + "url": "https://github.com/Codeception/phpunit-wrapper.git", + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/681b62348837a5ef07d10d8a226f5bc358cc8805", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", "shasum": "" }, "require": { - "phpunit/phpunit-mock-objects": ">2.3 <7.0" + "phpunit/php-code-coverage": ">=4.0.4 <6.0", + "phpunit/phpunit": ">=6.5.13 <7.0", + "sebastian/comparator": ">=1.2.4 <3.0", + "sebastian/diff": ">=1.4 <4.0" + }, + "replace": { + "codeception/phpunit-wrapper": "*" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <8.0" + "codeception/specify": "*", + "vlucas/phpdotenv": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Codeception\\PHPUnit\\": "src\\" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Davert", + "email": "davert.php@resend.cc" + } + ], + "description": "PHPUnit classes used by Codeception", + "time": "2019-02-26T20:47:39+00:00" + }, + { + "name": "codeception/stub", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Stub.git", + "reference": "853657f988942f7afb69becf3fd0059f192c705a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", + "reference": "853657f988942f7afb69becf3fd0059f192c705a", + "shasum": "" + }, + "require": { + "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" }, "type": "library", "autoload": { @@ -4912,25 +4960,25 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-05-17T09:31:08+00:00" + "time": "2019-03-02T15:35:10+00:00" }, { "name": "consolidation/annotated-command", - "version": "2.10.1", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "288593672a8ca9ead2c73a8bfbfa4737862bfd6a" + "reference": "512a2e54c98f3af377589de76c43b24652bcb789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/288593672a8ca9ead2c73a8bfbfa4737862bfd6a", - "reference": "288593672a8ca9ead2c73a8bfbfa4737862bfd6a", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/512a2e54c98f3af377589de76c43b24652bcb789", + "reference": "512a2e54c98f3af377589de76c43b24652bcb789", "shasum": "" }, "require": { "consolidation/output-formatters": "^3.4", - "php": ">=5.4.0", + "php": ">=5.4.5", "psr/log": "^1", "symfony/console": "^2.8|^3|^4", "symfony/event-dispatcher": "^2.5|^3|^4", @@ -5008,20 +5056,20 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-12-14T01:52:35+00:00" + "time": "2019-03-08T16:55:03+00:00" }, { "name": "consolidation/config", - "version": "1.1.1", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/consolidation/config.git", - "reference": "925231dfff32f05b787e1fddb265e789b939cf4c" + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/925231dfff32f05b787e1fddb265e789b939cf4c", - "reference": "925231dfff32f05b787e1fddb265e789b939cf4c", + "url": "https://api.github.com/repos/consolidation/config/zipball/cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", "shasum": "" }, "require": { @@ -5030,9 +5078,9 @@ "php": ">=5.4.0" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5", - "satooshi/php-coveralls": "^1.0", "squizlabs/php_codesniffer": "2.*", "symfony/console": "^2.5|^3|^4", "symfony/yaml": "^2.8.11|^3|^4" @@ -5042,6 +5090,33 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require-dev": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require-dev": { + "symfony/console": "^2.8", + "symfony/event-dispatcher": "^2.8", + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -5062,35 +5137,76 @@ } ], "description": "Provide configuration services for a commandline tool.", - "time": "2018-10-24T17:55:35+00:00" + "time": "2019-03-03T19:37:04+00:00" }, { "name": "consolidation/log", - "version": "1.0.6", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/consolidation/log.git", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395" + "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/dfd8189a771fe047bf3cd669111b2de5f1c79395", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395", + "url": "https://api.github.com/repos/consolidation/log/zipball/b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", + "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", "shasum": "" }, "require": { - "php": ">=5.5.0", - "psr/log": "~1.0", + "php": ">=5.4.5", + "psr/log": "^1.0", "symfony/console": "^2.8|^3|^4" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "2.*" + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^2" }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + }, + "phpunit4": { + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -5111,20 +5227,20 @@ } ], "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "time": "2018-05-25T18:14:39+00:00" + "time": "2019-01-01T17:30:51+00:00" }, { "name": "consolidation/output-formatters", - "version": "3.4.0", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19" + "reference": "0881112642ad9059071f13f397f571035b527cb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/a942680232094c4a5b21c0b7e54c20cce623ae19", - "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/0881112642ad9059071f13f397f571035b527cb9", + "reference": "0881112642ad9059071f13f397f571035b527cb9", "shasum": "" }, "require": { @@ -5134,11 +5250,10 @@ "symfony/finder": "^2.5|^3|^4" }, "require-dev": { - "g1a/composer-test-scenarios": "^2", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5.7.27", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7", - "symfony/console": "3.2.3", "symfony/var-dumper": "^2.8|^3|^4", "victorjonsson/markdowndocs": "^1.3" }, @@ -5147,6 +5262,52 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^6" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony3": { + "require": { + "symfony/console": "^3.4", + "symfony/finder": "^3.4", + "symfony/var-dumper": "^3.4" + }, + "config": { + "platform": { + "php": "5.6.32" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + }, + "scenario-options": { + "create-lockfile": "false" + } + } + }, "branch-alias": { "dev-master": "3.x-dev" } @@ -5167,25 +5328,25 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-10-19T22:35:38+00:00" + "time": "2019-03-14T03:45:44+00:00" }, { "name": "consolidation/robo", - "version": "1.3.3", + "version": "1.4.9", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "01789e1c4f3b0d7e5b4ee0aca1843a9438ff4983" + "reference": "5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/01789e1c4f3b0d7e5b4ee0aca1843a9438ff4983", - "reference": "01789e1c4f3b0d7e5b4ee0aca1843a9438ff4983", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345", + "reference": "5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345", "shasum": "" }, "require": { - "consolidation/annotated-command": "^2.10.1", - "consolidation/config": "^1.0.10", + "consolidation/annotated-command": "^2.10.2", + "consolidation/config": "^1.2", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", "consolidation/self-update": "^1", @@ -5211,7 +5372,7 @@ "natxet/cssmin": "3.0.4", "nikic/php-parser": "^3.1.5", "patchwork/jsqueeze": "~2", - "pear/archive_tar": "^1.4.2", + "pear/archive_tar": "^1.4.4", "php-coveralls/php-coveralls": "^1", "phpunit/php-code-coverage": "~2|~4", "squizlabs/php_codesniffer": "^2.8" @@ -5256,7 +5417,7 @@ } }, "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -5275,7 +5436,7 @@ } ], "description": "Modern task runner", - "time": "2018-12-14T02:54:30+00:00" + "time": "2019-03-19T18:07:19+00:00" }, { "name": "consolidation/self-update", @@ -5388,16 +5549,16 @@ }, { "name": "doctrine/annotations", - "version": "v1.6.0", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" + "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", - "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/53120e0eb10355388d6ccbe462f1fea34ddadb24", + "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24", "shasum": "" }, "require": { @@ -5452,38 +5613,40 @@ "docblock", "parser" ], - "time": "2017-12-06T07:11:42+00:00" + "time": "2019-03-25T19:12:02+00:00" }, { "name": "doctrine/collections", - "version": "v1.5.0", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf" + "reference": "d2ae4ef05e25197343b6a39bae1d3c427a2f6956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf", - "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d2ae4ef05e25197343b6a39bae1d3c427a2f6956", + "reference": "d2ae4ef05e25197343b6a39bae1d3c427a2f6956", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1.3" }, "require-dev": { - "doctrine/coding-standard": "~0.1@dev", - "phpunit/phpunit": "^5.7" + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan-shim": "^0.9.2", + "phpunit/phpunit": "^7.0", + "vimeo/psalm": "^3.2.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Collections\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" } }, "notification-url": "https://packagist.org/downloads/", @@ -5512,38 +5675,41 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", "keywords": [ "array", "collections", - "iterator" + "iterators", + "php" ], - "time": "2017-07-22T10:37:32+00:00" + "time": "2019-03-25T19:03:48+00:00" }, { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "a2c590166b2133a4633738648b6b064edae0814a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", + "reference": "a2c590166b2133a4633738648b6b064edae0814a", "shasum": "" }, "require": { "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { @@ -5568,12 +5734,12 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2019-03-17T17:37:11+00:00" }, { "name": "doctrine/lexer", @@ -5778,16 +5944,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.13.1", + "version": "v2.13.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "54814c62d5beef3ba55297b9b3186ed8b8a1b161" + "reference": "38d6f2e9be2aa80bf3c7365612af7f9eb9078719" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/54814c62d5beef3ba55297b9b3186ed8b8a1b161", - "reference": "54814c62d5beef3ba55297b9b3186ed8b8a1b161", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/38d6f2e9be2aa80bf3c7365612af7f9eb9078719", + "reference": "38d6f2e9be2aa80bf3c7365612af7f9eb9078719", "shasum": "" }, "require": { @@ -5814,7 +5980,7 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.1", + "keradus/cli-executor": "^1.2", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.1", "php-cs-fixer/accessible-object": "^1.0", @@ -5865,7 +6031,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2018-10-21T00:32:10+00:00" + "time": "2019-01-04T18:24:28+00:00" }, { "name": "fzaninotto/faker", @@ -6501,25 +6667,56 @@ ], "time": "2018-02-14T22:37:14+00:00" }, + { + "name": "magento/magento-coding-standard", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/magento/magento-coding-standard.git", + "reference": "489029a285c637825294e272d31c3f4ac00a454e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/489029a285c637825294e272d31c3f4ac00a454e", + "reference": "489029a285c637825294e272d31c3f4ac00a454e", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "squizlabs/php_codesniffer": "~3.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "A set of Magento specific PHP CodeSniffer rules.", + "time": "2019-04-01T17:03:33+00:00" + }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.3.12", + "version": "2.3.14", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "599004be3e14ebbe6fac77de2edbab934d70f19c" + "reference": "b4002b3fe53884895921b44cf519d42918e3c7c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/599004be3e14ebbe6fac77de2edbab934d70f19c", - "reference": "599004be3e14ebbe6fac77de2edbab934d70f19c", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/b4002b3fe53884895921b44cf519d42918e3c7c6", + "reference": "b4002b3fe53884895921b44cf519d42918e3c7c6", "shasum": "" }, "require": { "allure-framework/allure-codeception": "~1.3.0", - "codeception/codeception": "~2.3.4", + "codeception/codeception": "~2.3.4 || ~2.4.0 ", "consolidation/robo": "^1.0.0", "epfremme/swagger-php": "^2.0", + "ext-curl": "*", "flow/jsonpath": ">0.2", "fzaninotto/faker": "^1.6", "monolog/monolog": "^1.0", @@ -6536,6 +6733,7 @@ "goaop/framework": "2.2.0", "php-coveralls/php-coveralls": "^1.0", "phpmd/phpmd": "^2.6.0", + "phpunit/phpunit": "~6.5.0 || ~7.0.0", "rregeer/phpunit-coverage-check": "^0.1.4", "sebastian/phpcpd": "~3.0 || ~4.0", "squizlabs/php_codesniffer": "~3.2", @@ -6570,7 +6768,7 @@ "magento", "testing" ], - "time": "2018-12-19T17:04:11+00:00" + "time": "2019-02-19T16:03:22+00:00" }, { "name": "mikey179/vfsStream", @@ -7584,16 +7782,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.13", + "version": "6.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", "shasum": "" }, "require": { @@ -7664,7 +7862,7 @@ "testing", "xunit" ], - "time": "2018-09-08T15:10:43+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -7723,6 +7921,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { @@ -8466,16 +8665,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "db7e59fec9c82d45e745eb500e6ede2d96f4a6e9" + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/db7e59fec9c82d45e745eb500e6ede2d96f4a6e9", - "reference": "db7e59fec9c82d45e745eb500e6ede2d96f4a6e9", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/61d85c5af2fc058014c7c89504c3944e73a086f0", + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0", "shasum": "" }, "require": { @@ -8519,20 +8718,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-11-26T11:49:31+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/config", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "005d9a083d03f588677d15391a716b1ac9b887c0" + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/005d9a083d03f588677d15391a716b1ac9b887c0", - "reference": "005d9a083d03f588677d15391a716b1ac9b887c0", + "url": "https://api.github.com/repos/symfony/config/zipball/7f70d79c7a24a94f8e98abb988049403a53d7b31", + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31", "shasum": "" }, "require": { @@ -8582,7 +8781,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-11-30T22:21:14+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/contracts", @@ -8654,16 +8853,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "e4adc57a48d3fa7f394edfffa9e954086d7740e5" + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e4adc57a48d3fa7f394edfffa9e954086d7740e5", - "reference": "e4adc57a48d3fa7f394edfffa9e954086d7740e5", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cdadb3765df7c89ac93628743913b92bb91f1704", + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704", "shasum": "" }, "require": { @@ -8723,20 +8922,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-12-02T15:59:36+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "7438a32108fdd555295f443605d6de2cce473159" + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7438a32108fdd555295f443605d6de2cce473159", - "reference": "7438a32108fdd555295f443605d6de2cce473159", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb", + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb", "shasum": "" }, "require": { @@ -8780,20 +8979,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-11-26T10:55:26+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "1b31f3017fadd8cb05cf2c8aebdbf3b12a943851" + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1b31f3017fadd8cb05cf2c8aebdbf3b12a943851", - "reference": "1b31f3017fadd8cb05cf2c8aebdbf3b12a943851", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/850a667d6254ccf6c61d853407b16f21c4579c77", + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77", "shasum": "" }, "require": { @@ -8834,20 +9033,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-11-26T10:55:26+00:00" + "time": "2019-02-26T08:03:39+00:00" }, { "name": "symfony/options-resolver", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "a9c38e8a3da2c03b3e71fdffa6efb0bda51390ba" + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a9c38e8a3da2c03b3e71fdffa6efb0bda51390ba", - "reference": "a9c38e8a3da2c03b3e71fdffa6efb0bda51390ba", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3896e5a7d06fd15fa4947694c8dcdd371ff147d1", + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1", "shasum": "" }, "require": { @@ -8888,20 +9087,20 @@ "configuration", "options" ], - "time": "2018-11-11T19:52:12+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224" + "reference": "bc4858fb611bda58719124ca079baff854149c89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/6b88000cdd431cd2e940caa2cb569201f3f84224", - "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89", + "reference": "bc4858fb611bda58719124ca079baff854149c89", "shasum": "" }, "require": { @@ -8911,7 +9110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8947,20 +9146,20 @@ "portable", "shim" ], - "time": "2018-09-21T06:26:08+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631" + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9050816e2ca34a8e916c3a0ae8b9c2fccf68b631", - "reference": "9050816e2ca34a8e916c3a0ae8b9c2fccf68b631", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", "shasum": "" }, "require": { @@ -8969,7 +9168,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -9002,20 +9201,20 @@ "portable", "shim" ], - "time": "2018-09-21T13:07:52+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/stopwatch", - "version": "v4.2.1", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "ec076716412274e51f8a7ea675d9515e5c311123" + "reference": "b1a5f646d56a3290230dbc8edf2a0d62cda23f67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/ec076716412274e51f8a7ea675d9515e5c311123", - "reference": "ec076716412274e51f8a7ea675d9515e5c311123", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b1a5f646d56a3290230dbc8edf2a0d62cda23f67", + "reference": "b1a5f646d56a3290230dbc8edf2a0d62cda23f67", "shasum": "" }, "require": { @@ -9052,20 +9251,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-11-11T19:52:12+00:00" + "time": "2019-01-16T20:31:39+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.20", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "291e13d808bec481eab83f301f7bff3e699ef603" + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/291e13d808bec481eab83f301f7bff3e699ef603", - "reference": "291e13d808bec481eab83f301f7bff3e699ef603", + "url": "https://api.github.com/repos/symfony/yaml/zipball/57f1ce82c997f5a8701b89ef970e36bb657fd09c", + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c", "shasum": "" }, "require": { @@ -9111,7 +9310,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-11-11T19:48:54+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "theseer/fdomdocument", @@ -9195,20 +9394,21 @@ }, { "name": "vlucas/phpdotenv", - "version": "v2.5.1", + "version": "v2.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e" + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", - "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2a7dcf7e3e02dc5e701004e51a6f304b713107d5", + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "^1.9" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.0" @@ -9216,7 +9416,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -9241,24 +9441,25 @@ "env", "environment" ], - "time": "2018-07-29T20:33:41+00:00" + "time": "2019-01-29T11:11:52+00:00" }, { "name": "webmozart/assert", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "phpunit/phpunit": "^4.6", @@ -9291,7 +9492,7 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], diff --git a/dev/tests/acceptance/RoboFile.php b/dev/tests/acceptance/RoboFile.php deleted file mode 100644 index e6e9e591bbd8b..0000000000000 --- a/dev/tests/acceptance/RoboFile.php +++ /dev/null @@ -1,175 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -use Symfony\Component\Yaml\Yaml; - -/** This is project's console commands configuration for Robo task runner. - * - * @codingStandardsIgnoreStart - * @see http://robo.li/ - */ -class RoboFile extends \Robo\Tasks -{ - use Robo\Task\Base\loadShortcuts; - - /** - * Duplicate the Example configuration files for the Project. - * Build the Codeception project. - * - * @return void - */ - function buildProject() - { - passthru($this->getBaseCmd("build:project")); - } - - /** - * Generate all Tests in PHP OR Generate set of tests via passing array of tests - * - * @param array $tests - * @param array $opts - * @return \Robo\Result - */ - function generateTests(array $tests, $opts = [ - 'config' => null, - 'force' => false, - 'nodes' => null, - 'lines' => null, - 'tests' => null - ]) - { - $baseCmd = $this->getBaseCmd("generate:tests"); - - $mftfArgNames = ['config', 'nodes', 'lines', 'tests']; - // append arguments to the end of the command - foreach ($opts as $argName => $argValue) { - if (in_array($argName, $mftfArgNames) && $argValue !== null) { - $baseCmd .= " --$argName $argValue"; - } - } - - // use a separate conditional for the force flag (casting bool to string in php is hard) - if ($opts['force']) { - $baseCmd .= ' --force'; - } - - return $this->taskExec($baseCmd)->args($tests)->run(); - } - - /** - * Generate a suite based on name(s) passed in as args. - * - * @param array $args - * @throws Exception - * @return \Robo\Result - */ - function generateSuite(array $args) - { - if (empty($args)) { - throw new Exception("Please provide suite name(s) after generate:suite command"); - } - $baseCmd = $this->getBaseCmd("generate:suite"); - return $this->taskExec($baseCmd)->args($args)->run(); - } - - /** - * Run all Tests with the specified @group tag'. - * - * @param array $args - * @return \Robo\Result - */ - function group(array $args) - { - $args = array_merge($args, ['-k']); - $baseCmd = $this->getBaseCmd("run:group"); - return $this->taskExec($baseCmd)->args($args)->run(); - } - - /** - * Generate the HTML for the Allure report based on the Test XML output - Allure v1.4.X - * - * @return \Robo\Result - */ - function allure1Generate() - { - return $this->_exec('allure generate tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-results'. DIRECTORY_SEPARATOR .' -o tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .''); - } - - /** - * Generate the HTML for the Allure report based on the Test XML output - Allure v2.3.X - * - * @return \Robo\Result - */ - function allure2Generate() - { - return $this->_exec('allure generate tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-results'. DIRECTORY_SEPARATOR .' --output tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .' --clean'); - } - - /** - * Open the HTML Allure report - Allure v1.4.X - * - * @return \Robo\Result - */ - function allure1Open() - { - return $this->_exec('allure report open --report-dir tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .''); - } - - /** - * Open the HTML Allure report - Allure v2.3.X - * - * @return \Robo\Result - */ - function allure2Open() - { - return $this->_exec('allure open --port 0 tests'. DIRECTORY_SEPARATOR .'_output'. DIRECTORY_SEPARATOR .'allure-report'. DIRECTORY_SEPARATOR .''); - } - - /** - * Generate and open the HTML Allure report - Allure v1.4.X - * - * @return \Robo\Result - */ - function allure1Report() - { - $result1 = $this->allure1Generate(); - - if ($result1->wasSuccessful()) { - return $this->allure1Open(); - } else { - return $result1; - } - } - - /** - * Generate and open the HTML Allure report - Allure v2.3.X - * - * @return \Robo\Result - */ - function allure2Report() - { - $result1 = $this->allure2Generate(); - - if ($result1->wasSuccessful()) { - return $this->allure2Open(); - } else { - return $result1; - } - } - - /** - * Private function for returning the formatted command for the passthru to mftf bin execution. - * - * @param string $command - * @return string - */ - private function getBaseCmd($command) - { - $this->writeln("\033[01;31m Use of robo will be deprecated with next major release, please use <root>/vendor/bin/mftf $command \033[0m"); - chdir(__DIR__); - return realpath('../../../vendor/bin/mftf') . " $command"; - } -} \ No newline at end of file diff --git a/dev/tests/acceptance/composer.json b/dev/tests/acceptance/composer.json deleted file mode 100755 index 83cad123f8568..0000000000000 --- a/dev/tests/acceptance/composer.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "description": "Magento 2 (Open Source) Functional Tests", - "type": "project", - "version": "1.0.0-dev", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "config": { - "sort-packages": true - }, - "require": { - "php": "~7.1.3||~7.2.0", - "codeception/codeception": "~2.3.4 || ~2.4.0", - "consolidation/robo": "^1.0.0", - "vlucas/phpdotenv": "^2.4" - }, - "autoload": { - "psr-4": { - "Magento\\": "tests/functional/Magento" - }, - "files": ["tests/_bootstrap.php"] - }, - "prefer-stable": true -} diff --git a/dev/tests/acceptance/composer.lock b/dev/tests/acceptance/composer.lock deleted file mode 100644 index 8542402a98f50..0000000000000 --- a/dev/tests/acceptance/composer.lock +++ /dev/null @@ -1,3262 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "content-hash": "46ca2d50566f5069daef753664080c5a", - "packages": [ - { - "name": "behat/gherkin", - "version": "v4.4.5", - "source": { - "type": "git", - "url": "https://github.com/Behat/Gherkin.git", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", - "shasum": "" - }, - "require": { - "php": ">=5.3.1" - }, - "require-dev": { - "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3", - "symfony/yaml": "~2.3|~3" - }, - "suggest": { - "symfony/yaml": "If you want to parse features, represented in YAML files" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, - "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - } - ], - "description": "Gherkin DSL parser for PHP 5.3", - "homepage": "http://behat.org/", - "keywords": [ - "BDD", - "Behat", - "Cucumber", - "DSL", - "gherkin", - "parser" - ], - "time": "2016-10-30T11:50:56+00:00" - }, - { - "name": "codeception/codeception", - "version": "2.3.9", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "shasum": "" - }, - "require": { - "behat/gherkin": "~4.4.0", - "codeception/stub": "^1.0", - "ext-json": "*", - "ext-mbstring": "*", - "facebook/webdriver": ">=1.1.3 <2.0", - "guzzlehttp/guzzle": ">=4.1.4 <7.0", - "guzzlehttp/psr7": "~1.0", - "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <3.0", - "symfony/browser-kit": ">=2.7 <5.0", - "symfony/console": ">=2.7 <5.0", - "symfony/css-selector": ">=2.7 <5.0", - "symfony/dom-crawler": ">=2.7 <5.0", - "symfony/event-dispatcher": ">=2.7 <5.0", - "symfony/finder": ">=2.7 <5.0", - "symfony/yaml": ">=2.7 <5.0" - }, - "require-dev": { - "codeception/specify": "~0.3", - "facebook/graph-sdk": "~5.3", - "flow/jsonpath": "~0.2", - "monolog/monolog": "~1.8", - "pda/pheanstalk": "~3.0", - "php-amqplib/php-amqplib": "~2.4", - "predis/predis": "^1.0", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <5.0", - "vlucas/phpdotenv": "^2.4.0" - }, - "suggest": { - "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module", - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests", - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "flow/jsonpath": "For using JSONPath in REST module", - "league/factory-muffin": "For DataFactory module", - "league/factory-muffin-faker": "For Faker support in DataFactory module", - "phpseclib/phpseclib": "for SFTP option in FTP Module", - "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" - }, - "bin": [ - "codecept" - ], - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-4": { - "Codeception\\": "src\\Codeception", - "Codeception\\Extension\\": "ext" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - } - ], - "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", - "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" - ], - "time": "2018-02-26T23:29:41+00:00" - }, - { - "name": "codeception/stub", - "version": "1.0.4", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/681b62348837a5ef07d10d8a226f5bc358cc8805", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805", - "shasum": "" - }, - "require": { - "phpunit/phpunit-mock-objects": ">2.3 <7.0" - }, - "require-dev": { - "phpunit/phpunit": ">=4.8 <8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-05-17T09:31:08+00:00" - }, - { - "name": "consolidation/annotated-command", - "version": "2.8.4", - "source": { - "type": "git", - "url": "https://github.com/consolidation/annotated-command.git", - "reference": "651541a0b68318a2a202bda558a676e5ad92223c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/651541a0b68318a2a202bda558a676e5ad92223c", - "reference": "651541a0b68318a2a202bda558a676e5ad92223c", - "shasum": "" - }, - "require": { - "consolidation/output-formatters": "^3.1.12", - "php": ">=5.4.0", - "psr/log": "^1", - "symfony/console": "^2.8|^3|^4", - "symfony/event-dispatcher": "^2.5|^3|^4", - "symfony/finder": "^2.5|^3|^4" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^2", - "phpunit/phpunit": "^6", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\AnnotatedCommand\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-05-25T18:04:25+00:00" - }, - { - "name": "consolidation/config", - "version": "1.0.11", - "source": { - "type": "git", - "url": "https://github.com/consolidation/config.git", - "reference": "ede41d946078e97e7a9513aadc3352f1c26817af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/ede41d946078e97e7a9513aadc3352f1c26817af", - "reference": "ede41d946078e97e7a9513aadc3352f1c26817af", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "grasmash/expander": "^1", - "php": ">=5.4.0" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4", - "satooshi/php-coveralls": "^1.0", - "squizlabs/php_codesniffer": "2.*", - "symfony/console": "^2.5|^3|^4", - "symfony/yaml": "^2.8.11|^3|^4" - }, - "suggest": { - "symfony/yaml": "Required to use Consolidation\\Config\\Loader\\YamlConfigLoader" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\Config\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Provide configuration services for a commandline tool.", - "time": "2018-05-27T01:17:02+00:00" - }, - { - "name": "consolidation/log", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/consolidation/log.git", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/dfd8189a771fe047bf3cd669111b2de5f1c79395", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395", - "shasum": "" - }, - "require": { - "php": ">=5.5.0", - "psr/log": "~1.0", - "symfony/console": "^2.8|^3|^4" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "2.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "time": "2018-05-25T18:14:39+00:00" - }, - { - "name": "consolidation/output-formatters", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/consolidation/output-formatters.git", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "symfony/console": "^2.8|^3|^4", - "symfony/finder": "^2.5|^3|^4" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^2", - "phpunit/phpunit": "^5.7.27", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "^2.7", - "symfony/console": "3.2.3", - "symfony/var-dumper": "^2.8|^3|^4", - "victorjonsson/markdowndocs": "^1.3" - }, - "suggest": { - "symfony/var-dumper": "For using the var_dump formatter" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\OutputFormatters\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-05-25T18:02:34+00:00" - }, - { - "name": "consolidation/robo", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/consolidation/Robo.git", - "reference": "ac563abfadf7cb7314b4e152f2b5033a6c255f6f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/ac563abfadf7cb7314b4e152f2b5033a6c255f6f", - "reference": "ac563abfadf7cb7314b4e152f2b5033a6c255f6f", - "shasum": "" - }, - "require": { - "consolidation/annotated-command": "^2.8.2", - "consolidation/config": "^1.0.10", - "consolidation/log": "~1", - "consolidation/output-formatters": "^3.1.13", - "grasmash/yaml-expander": "^1.3", - "league/container": "^2.2", - "php": ">=5.5.0", - "symfony/console": "^2.8|^3|^4", - "symfony/event-dispatcher": "^2.5|^3|^4", - "symfony/filesystem": "^2.5|^3|^4", - "symfony/finder": "^2.5|^3|^4", - "symfony/process": "^2.5|^3|^4" - }, - "replace": { - "codegyre/robo": "< 1.0" - }, - "require-dev": { - "codeception/aspect-mock": "^1|^2.1.1", - "codeception/base": "^2.3.7", - "codeception/verify": "^0.3.2", - "g1a/composer-test-scenarios": "^2", - "goaop/framework": "~2.1.2", - "goaop/parser-reflection": "^1.1.0", - "natxet/cssmin": "3.0.4", - "nikic/php-parser": "^3.1.5", - "patchwork/jsqueeze": "~2", - "pear/archive_tar": "^1.4.2", - "phpunit/php-code-coverage": "~2|~4", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "^2.8" - }, - "suggest": { - "henrikbjorn/lurker": "For monitoring filesystem changes in taskWatch", - "natxet/CssMin": "For minifying CSS files in taskMinify", - "patchwork/jsqueeze": "For minifying JS files in taskMinify", - "pear/archive_tar": "Allows tar archives to be created and extracted in taskPack and taskExtract, respectively." - }, - "bin": [ - "robo" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev", - "dev-state": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Robo\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Davert", - "email": "davert.php@resend.cc" - } - ], - "description": "Modern task runner", - "time": "2018-05-27T01:42:53+00:00" - }, - { - "name": "container-interop/container-interop", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/container-interop/container-interop.git", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "shasum": "" - }, - "require": { - "psr/container": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Interop\\Container\\": "src/Interop/Container/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", - "homepage": "https://github.com/container-interop/container-interop", - "time": "2017-02-14T19:40:03+00:00" - }, - { - "name": "dflydev/dot-access-data", - "version": "v1.1.0", - "source": { - "type": "git", - "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/3fbd874921ab2c041e899d044585a2ab9795df8a", - "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Dflydev\\DotAccessData": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dragonfly Development Inc.", - "email": "info@dflydev.com", - "homepage": "http://dflydev.com" - }, - { - "name": "Beau Simensen", - "email": "beau@dflydev.com", - "homepage": "http://beausimensen.com" - }, - { - "name": "Carlos Frutos", - "email": "carlos@kiwing.it", - "homepage": "https://github.com/cfrutos" - } - ], - "description": "Given a deep data structure, access data by dot notation.", - "homepage": "https://github.com/dflydev/dflydev-dot-access-data", - "keywords": [ - "access", - "data", - "dot", - "notation" - ], - "time": "2017-01-20T21:14:22+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2017-07-22T11:58:36+00:00" - }, - { - "name": "facebook/webdriver", - "version": "1.6.0", - "source": { - "type": "git", - "url": "https://github.com/facebook/php-webdriver.git", - "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/bd8c740097eb9f2fc3735250fc1912bc811a954e", - "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-zip": "*", - "php": "^5.6 || ~7.0", - "symfony/process": "^2.8 || ^3.1 || ^4.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.0", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "php-coveralls/php-coveralls": "^2.0", - "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "^5.7", - "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", - "squizlabs/php_codesniffer": "^2.6", - "symfony/var-dumper": "^3.3 || ^4.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-community": "1.5-dev" - } - }, - "autoload": { - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "A PHP client for Selenium WebDriver", - "homepage": "https://github.com/facebook/php-webdriver", - "keywords": [ - "facebook", - "php", - "selenium", - "webdriver" - ], - "time": "2018-05-16T17:37:13+00:00" - }, - { - "name": "grasmash/expander", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/grasmash/expander.git", - "reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/grasmash/expander/zipball/95d6037344a4be1dd5f8e0b0b2571a28c397578f", - "reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "php": ">=5.4" - }, - "require-dev": { - "greg-1-anderson/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4|^5.5.4", - "satooshi/php-coveralls": "^1.0.2|dev-master", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Grasmash\\Expander\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matthew Grasmick" - } - ], - "description": "Expands internal property references in PHP arrays file.", - "time": "2017-12-21T22:14:55+00:00" - }, - { - "name": "grasmash/yaml-expander", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/grasmash/yaml-expander.git", - "reference": "3f0f6001ae707a24f4d9733958d77d92bf9693b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/grasmash/yaml-expander/zipball/3f0f6001ae707a24f4d9733958d77d92bf9693b1", - "reference": "3f0f6001ae707a24f4d9733958d77d92bf9693b1", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "php": ">=5.4", - "symfony/yaml": "^2.8.11|^3|^4" - }, - "require-dev": { - "greg-1-anderson/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4.8|^5.5.4", - "satooshi/php-coveralls": "^1.0.2|dev-master", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Grasmash\\YamlExpander\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matthew Grasmick" - } - ], - "description": "Expands internal property references in a yaml file.", - "time": "2017-12-16T16:06:03+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "6.3.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "shasum": "" - }, - "require": { - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.3-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2018-04-22T15:46:56+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "time": "2016-12-20T10:07:11+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Schultze", - "homepage": "https://github.com/Tobion" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "request", - "response", - "stream", - "uri", - "url" - ], - "time": "2017-03-20T17:10:46+00:00" - }, - { - "name": "league/container", - "version": "2.4.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/container.git", - "reference": "43f35abd03a12977a60ffd7095efd6a7808488c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/container/zipball/43f35abd03a12977a60ffd7095efd6a7808488c0", - "reference": "43f35abd03a12977a60ffd7095efd6a7808488c0", - "shasum": "" - }, - "require": { - "container-interop/container-interop": "^1.2", - "php": "^5.4.0 || ^7.0" - }, - "provide": { - "container-interop/container-interop-implementation": "^1.2", - "psr/container-implementation": "^1.0" - }, - "replace": { - "orno/di": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Container\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phil Bennett", - "email": "philipobenito@gmail.com", - "homepage": "http://www.philipobenito.com", - "role": "Developer" - } - ], - "description": "A fast and intuitive dependency injection container.", - "homepage": "https://github.com/thephpleague/container", - "keywords": [ - "container", - "dependency", - "di", - "injection", - "league", - "provider", - "service" - ], - "time": "2017-05-10T09:20:27+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.8.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2018-06-11T23:09:50+00:00" - }, - { - "name": "phar-io/manifest", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "phar-io/version": "^1.0.1", - "php": "^5.6 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2017-03-05T18:14:27+00:00" - }, - { - "name": "phar-io/version", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "time": "2017-03-05T17:38:23+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2017-09-11T18:02:19+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", - "shasum": "" - }, - "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "doctrine/instantiator": "~1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-07-14T14:27:02+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.7.6", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2018-04-18T13:57:24+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "5.3.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^7.0", - "phpunit/php-file-iterator": "^1.4.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.0", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-xdebug": "^2.5.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2018-04-06T15:36:58+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2017-11-27T13:52:08+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-11-27T05:48:46+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "6.5.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "093ca5508174cd8ab8efe44fd1dde447adfdec8f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/093ca5508174cd8ab8efe44fd1dde447adfdec8f", - "reference": "093ca5508174cd8ab8efe44fd1dde447adfdec8f", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.0", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", - "phpunit/php-file-iterator": "^1.4.3", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", - "sebastian/environment": "^3.1", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^1.0", - "sebastian/version": "^2.0.1" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.5.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2018-07-03T06:40:40+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "5.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "6f9a3c8bf34188a2b53ce2ae7a126089c53e0a9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/6f9a3c8bf34188a2b53ce2ae7a126089c53e0a9f", - "reference": "6f9a3c8bf34188a2b53ce2ae7a126089c53e0a9f", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.0", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" - }, - "conflict": { - "phpunit/phpunit": "<6.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.5" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2018-07-13T03:27:23+00:00" - }, - { - "name": "psr/container", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "time": "2017-02-14T16:28:37+00:00" - }, - { - "name": "psr/http-message", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "time": "2016-08-06T14:39:51+00:00" - }, - { - "name": "psr/log", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2016-10-10T12:19:37+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" - }, - { - "name": "sebastian/comparator", - "version": "2.1.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2018-02-01T13:46:46+00:00" - }, - { - "name": "sebastian/diff", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-08-03T08:09:46+00:00" - }, - { - "name": "sebastian/environment", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2017-07-01T08:51:00+00:00" - }, - { - "name": "sebastian/exporter", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2017-04-03T13:19:02+00:00" - }, - { - "name": "sebastian/global-state", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2017-04-27T15:39:26+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-08-03T12:35:26+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2017-03-29T09:07:27+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-03T06:23:57+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "symfony/browser-kit", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "ff9ac5d5808a530b2e7f6abcf3a2412d4f9bcd62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ff9ac5d5808a530b2e7f6abcf3a2412d4f9bcd62", - "reference": "ff9ac5d5808a530b2e7f6abcf3a2412d4f9bcd62", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/dom-crawler": "~3.4|~4.0" - }, - "require-dev": { - "symfony/css-selector": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0" - }, - "suggest": { - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony BrowserKit Component", - "homepage": "https://symfony.com", - "time": "2018-06-04T17:31:56+00:00" - }, - { - "name": "symfony/console", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "5c31f6a97c1c240707f6d786e7e59bfacdbc0219" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5c31f6a97c1c240707f6d786e7e59bfacdbc0219", - "reference": "5c31f6a97c1c240707f6d786e7e59bfacdbc0219", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/process": "<3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~3.4|~4.0", - "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0" - }, - "suggest": { - "psr/log-implementation": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2018-07-16T14:05:40+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "03ac71606ecb0b0ce792faa17d74cc32c2949ef4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/03ac71606ecb0b0ce792faa17d74cc32c2949ef4", - "reference": "03ac71606ecb0b0ce792faa17d74cc32c2949ef4", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "symfony/dom-crawler", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "eb501fa8aab8c8e2db790d8d0f945697769f6c41" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/eb501fa8aab8c8e2db790d8d0f945697769f6c41", - "reference": "eb501fa8aab8c8e2db790d8d0f945697769f6c41", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "symfony/css-selector": "~3.4|~4.0" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony DomCrawler Component", - "homepage": "https://symfony.com", - "time": "2018-07-05T11:54:23+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f", - "reference": "00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "conflict": { - "symfony/dependency-injection": "<3.4" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/expression-language": "~3.4|~4.0", - "symfony/stopwatch": "~3.4|~4.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2018-07-10T11:02:47+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/562bf7005b55fd80d26b582d28e3e10f2dd5ae9c", - "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "symfony/finder", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/84714b8417d19e4ba02ea78a41a975b3efaafddb", - "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "https://symfony.com", - "time": "2018-06-19T21:38:16+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2018-04-30T19:57:29+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/process", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", - "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2018-05-31T10:17:53+00:00" - }, - { - "name": "symfony/yaml", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2017-04-07T12:08:54+00:00" - }, - { - "name": "vlucas/phpdotenv", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "6ae3e2e6494bb5e58c2decadafc3de7f1453f70a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/6ae3e2e6494bb5e58c2decadafc3de7f1453f70a", - "reference": "6ae3e2e6494bb5e58c2decadafc3de7f1453f70a", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-4": { - "Dotenv\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "http://www.vancelucas.com" - } - ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", - "keywords": [ - "dotenv", - "env", - "environment" - ], - "time": "2018-07-01T10:25:50+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2018-01-29T19:49:41+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": true, - "prefer-lowest": false, - "platform": { - "php": "~7.1.3||~7.2.0" - }, - "platform-dev": [] -} diff --git a/dev/tests/acceptance/tests/_data/catalog_import_products.csv b/dev/tests/acceptance/tests/_data/catalog_import_products.csv new file mode 100644 index 0000000000000..7732f15d4ce3a --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_import_products.csv @@ -0,0 +1,4 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,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_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +SimpleProductForTest1,,Default,simple,"Default","base,second_website",SimpleProductAfterImport1,,,1.0000,1,"Taxable Goods","Catalog, Search",250.0000,,,,simple-product-for-test-1,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest2,,Default,simple,"Default",base,SimpleProductAfterImport2,,,1.0000,1,"Taxable Goods","Catalog, Search",300.0000,,,,simple-product-for-test-2,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest3,,Default,simple,"Default","base,second_website",SimpleProductAfterImport3,,,1.0000,1,"Taxable Goods","Catalog, Search",350.0000,,,,simple-product-for-test-3,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/dev/tests/acceptance/tests/_data/import_updated.csv b/dev/tests/acceptance/tests/_data/import_updated.csv new file mode 100644 index 0000000000000..b517150eec840 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/import_updated.csv @@ -0,0 +1,4 @@ +product_websites,store_view_code,attribute_set_code,product_type,categories,sku,price,name,url_key +base,,Default,simple,Default Category/category-admin,productformagetwo68980,123,productformagetwo68980,productformagetwo68980 +,en,Default,simple,,productformagetwo68980,,productformagetwo68980-english,productformagetwo68980-english +,nl,Default,simple,,productformagetwo68980,,productformagetwo68980-dutch,productformagetwo68980-dutch diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml index f5cd41bda74d7..f0bfec543f281 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CGuestUserTest"> <!-- Search configurable product --> <comment userInput="Search configurable product" stepKey="commentSearchConfigurableProduct" after="searchAssertSimpleProduct2ImageNotDefault" /> @@ -26,5 +26,5 @@ <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchGrabConfigProductPageImageSrc" after="searchAssertConfigProductPage"/> <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchGrabConfigProductPageImageSrc" stepKey="searchAssertConfigProductPageImageNotDefault" after="searchGrabConfigProductPageImageSrc"/> - </test> + </test> </tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml index 3e386a034eecc..9fe70c8b4dd3b 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CLoggedInUserTest"> <!-- Search configurable product --> <comment userInput="Search configurable product" stepKey="commentSearchConfigurableProduct" after="searchAssertSimpleProduct2ImageNotDefault" /> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml index d3b009eecf877..cb3d9edbc1cbb 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CLoggedInUserTest"> <!-- Step 5: Add products to wishlist --> <!-- Add Configurable Product to wishlist --> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml b/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml index 3bd64a7aff728..8254a9d8e92cf 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml @@ -31,4 +31,16 @@ </join> </attribute> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="orderApiTestAttribute" type="Magento\User\Api\Data\UserInterface"> + <join reference_table="admin_user" + join_on_field="store_id" + reference_field="user_id" + > + <field>firstname</field> + <field>lastname</field> + <field>email</field> + </join> + </attribute> + </extension_attributes> </config> diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php index 5458b5cfbb731..e18a8c8e97c79 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php @@ -51,31 +51,69 @@ public function __construct( * @return array|string|int|float|bool * @throws \Exception */ - public function postQuery(string $query, array $variables = [], string $operationName = '', array $headers = []) + public function post(string $query, array $variables = [], string $operationName = '', array $headers = []) { $url = $this->getEndpointUrl(); $headers = array_merge($headers, ['Accept: application/json', 'Content-Type: application/json']); $requestArray = [ 'query' => $query, - 'variables' => empty($variables) ? $variables : null, - 'operationName' => empty($operationName) ? $operationName : null + 'variables' => !empty($variables) ? $variables : null, + 'operationName' => !empty($operationName) ? $operationName : null ]; $postData = $this->json->jsonEncode($requestArray); $responseBody = $this->curlClient->post($url, $postData, $headers); - $responseBodyArray = $this->json->jsonDecode($responseBody); + return $this->processResponse($responseBody); + } + + /** + * Perform HTTP GET request for query + * + * @param string $query + * @param array $variables + * @param string $operationName + * @param array $headers + * @return mixed + * @throws \Exception + */ + public function get(string $query, array $variables = [], string $operationName = '', array $headers = []) + { + $url = $this->getEndpointUrl(); + $requestArray = [ + 'query' => $query, + 'variables' => $variables ? $this->json->jsonEncode($variables) : null, + 'operationName' => $operationName ?? null + ]; + array_filter($requestArray); + + $responseBody = $this->curlClient->get($url, $requestArray, $headers); + return $this->processResponse($responseBody); + } + + /** + * Process response from GraphQl server + * + * @param string $response + * @return mixed + * @throws \Exception + */ + private function processResponse(string $response) + { + $responseArray = $this->json->jsonDecode($response); - if (!is_array($responseBodyArray)) { - throw new \Exception('Unknown GraphQL response body: ' . json_encode($responseBodyArray)); + if (!is_array($responseArray)) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('Unknown GraphQL response body: ' . $response); } - $this->processErrors($responseBodyArray); + $this->processErrors($responseArray); - if (!isset($responseBodyArray['data'])) { - throw new \Exception('Unknown GraphQL response body: ' . json_encode($responseBodyArray)); - } else { - return $responseBodyArray['data']; + if (!isset($responseArray['data'])) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('Unknown GraphQL response body: ' . $response); } + + return $responseArray['data']; } /** @@ -107,6 +145,7 @@ private function processErrors($responseBodyArray) $responseBodyArray ); } + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('GraphQL responded with an unknown error: ' . json_encode($responseBodyArray)); } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php index 790581c476da1..8abd97b4b744d 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php @@ -6,6 +6,7 @@ namespace Magento\TestFramework\TestCase; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http; /** * Test case for Web API functional tests for Graphql. @@ -27,7 +28,7 @@ abstract class GraphQlAbstract extends WebapiAbstract private $appCache; /** - * Perform GraphQL call to the system under test. + * Perform GraphQL query call via GET to the system under test. * * @see \Magento\TestFramework\TestCase\GraphQl\Client::call() * @param string $query @@ -43,7 +44,32 @@ public function graphQlQuery( string $operationName = '', array $headers = [] ) { - return $this->getGraphQlClient()->postQuery( + return $this->getGraphQlClient()->get( + $query, + $variables, + $operationName, + $this->composeHeaders($headers) + ); + } + + /** + * Perform GraphQL mutations call via POST to the system under test. + * + * @see \Magento\TestFramework\TestCase\GraphQl\Client::call() + * @param string $query + * @param array $variables + * @param string $operationName + * @param array $headers + * @return array|int|string|float|bool GraphQL call results + * @throws \Exception + */ + public function graphQlMutation( + string $query, + array $variables = [], + string $operationName = '', + array $headers = [] + ) { + return $this->getGraphQlClient()->post( $query, $variables, $operationName, diff --git a/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php b/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php index 6fd7551676660..4bf1335f20667 100644 --- a/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php @@ -95,6 +95,6 @@ public function testGetAll() */ private function isTestBaseUrlSecure() { - return strpos('https://', TESTS_BASE_URL) !== false; + return strpos(TESTS_BASE_URL, 'https://') !== false; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php index c335b66505b0e..f3be684f93a4d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php @@ -146,6 +146,10 @@ public function testGetList() public function testSave($optionData) { $productSku = 'simple'; + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class + ); $optionDataPost = $optionData; $optionDataPost['product_sku'] = $productSku; @@ -162,6 +166,7 @@ public function testSave($optionData) ]; $result = $this->_webApiCall($serviceInfo, ['option' => $optionDataPost]); + $product = $productRepository->get($productSku); unset($result['product_sku']); unset($result['option_id']); if (!empty($result['values'])) { @@ -169,7 +174,12 @@ public function testSave($optionData) unset($result['values'][$key]['option_type_id']); } } + $this->assertEquals($optionData, $result); + $this->assertTrue($product->getHasOptions() == 1); + if ($optionDataPost['is_require']) { + $this->assertTrue($product->getRequiredOptions() == 1); + } } public function optionDataProvider() diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php index 6b8388e2f4345..237574dd6e22a 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php @@ -45,6 +45,9 @@ public function testAdd($optionData) $this->assertNotNull($response); $updatedData = $this->getAttributeOptions($testAttributeCode); $lastOption = array_pop($updatedData); + foreach ($updatedData as $option) { + $this->assertNotContains('id', $option['value']); + } $this->assertEquals( $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], $lastOption['label'] diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php index 4608b255459b6..769abadf20585 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php @@ -51,11 +51,13 @@ protected function getLinkData() 'link_type' => 'file', 'link_file_content' => [ 'name' => 'link1_content.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'link1_sample.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ], @@ -114,6 +116,7 @@ protected function getSampleData() 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'sample2.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ], @@ -146,7 +149,9 @@ protected function createDownloadableProduct() "price" => 10, 'attribute_set_id' => 4, "extension_attributes" => [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction "downloadable_product_links" => array_values($this->getLinkData()), + // phpcs:ignore Magento2.Functions.DiscouragedFunction "downloadable_product_samples" => array_values($this->getSampleData()), ], ]; @@ -301,11 +306,13 @@ public function testUpdateDownloadableProductLinksWithNewFile() 'link_type' => 'file', 'link_file_content' => [ 'name' => $linkFile . $extension, + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], 'sample_type' => 'file', 'sample_file_content' => [ 'name' => $sampleFile . $extension, + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ]; @@ -319,11 +326,13 @@ public function testUpdateDownloadableProductLinksWithNewFile() 'link_type' => 'file', 'link_file_content' => [ 'name' => 'link2_content.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'link2_sample.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ]; @@ -463,6 +472,7 @@ public function testUpdateDownloadableProductSamplesWithNewFile() 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'sample1.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ]; @@ -472,6 +482,11 @@ public function testUpdateDownloadableProductSamplesWithNewFile() 'title' => 'sample2_updated', 'sort_order' => 2, 'sample_type' => 'file', + 'sample_file_content' => [ + 'name' => 'sample2.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'file_data' => base64_encode(file_get_contents($this->testImagePath)), + ], ]; $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["downloadable_product_samples"] = @@ -606,7 +621,7 @@ protected function deleteProductBySku($productSku) protected function saveProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + for ($i = 0, $iMax = count($product['custom_attributes']); $i < $iMax; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.php index eddd456a7b866..46309c6d97dfa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.php @@ -7,11 +7,16 @@ namespace Magento\GraphQl\Catalog; -use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\Product\Attribute\Source\Status as productStatus; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; /** * Class CategoryProductsCountTest @@ -25,10 +30,39 @@ class CategoryProductsCountTest extends GraphQlAbstract */ private $productRepository; + /** + * @var Config $config + */ + private $resourceConfig; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + + /** + * @var CategoryLinkManagementInterface + */ + private $categoryLinkManagement; + + /** + * @inheritdoc + */ protected function setUp() { - $objectManager = Bootstrap::getObjectManager(); + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); $this->productRepository = $objectManager->create(ProductRepositoryInterface::class); + $this->resourceConfig = $objectManager->get(Config::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); } /** @@ -82,6 +116,15 @@ public function testCategoryWithInvisibleProduct() public function testCategoryWithOutOfStockProductManageStockEnabled() { $categoryId = 2; + $sku = 'simple-out-of-stock'; + $manageStock = $this->scopeConfig->getValue(Configuration::XML_PATH_MANAGE_STOCK); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, 1); + $this->reinitConfig->reinit(); + + // need to resave product to reindex it with new configuration. + $product = $this->productRepository->get($sku); + $this->productRepository->save($product); $query = <<<QUERY { @@ -93,15 +136,27 @@ public function testCategoryWithOutOfStockProductManageStockEnabled() QUERY; $response = $this->graphQlQuery($query); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, $manageStock); + $this->reinitConfig->reinit(); + self::assertEquals(0, $response['category']['product_count']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/category_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php */ public function testCategoryWithOutOfStockProductManageStockDisabled() { - $categoryId = 333; + $categoryId = 2; + $sku = 'simple-out-of-stock'; + $manageStock = $this->scopeConfig->getValue(Configuration::XML_PATH_MANAGE_STOCK); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, 0); + $this->reinitConfig->reinit(); + + // need to resave product to reindex it with new configuration. + $product = $this->productRepository->get($sku); + $this->productRepository->save($product); $query = <<<QUERY { @@ -113,6 +168,9 @@ public function testCategoryWithOutOfStockProductManageStockDisabled() QUERY; $response = $this->graphQlQuery($query); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, $manageStock); + $this->reinitConfig->reinit(); + self::assertEquals(1, $response['category']['product_count']); } @@ -140,4 +198,84 @@ public function testCategoryWithDisabledProduct() self::assertEquals(0, $response['category']['product_count']); } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + */ + public function testCategoryWithOutOfStockProductShowOutOfStockProduct() + { + $showOutOfStock = $this->scopeConfig->getValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, 1); + $this->reinitConfig->reinit(); + + $categoryId = 2; + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, $showOutOfStock); + $this->reinitConfig->reinit(); + + self::assertEquals(1, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/CatalogRule/_files/configurable_product.php + */ + public function testCategoryWithConfigurableChildrenOutOfStock() + { + $categoryId = 2; + + $this->categoryLinkManagement->assignProductToCategories('configurable', [$categoryId]); + + foreach (['simple1', 'simple2'] as $sku) { + $product = $this->productRepository->get($sku); + $product->setStockData(['is_in_stock' => 0]); + $this->productRepository->save($product); + } + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(0, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category_product.php + */ + public function testCategoryWithProductNotAvailableOnWebsite() + { + $product = $this->productRepository->getById(333); + $product->setWebsiteIds([]); + $this->productRepository->save($product); + + $categoryId = 333; + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(0, $response['category']['product_count']); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php index 6b57f84e4b9c4..f62be7328481c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php @@ -18,13 +18,11 @@ class CategoryProductsVariantsTest extends GraphQlAbstract { /** - * * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testGetSimpleProductsFromCategory() { - $query = <<<QUERY { 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 54e98367ab8ca..b2ce0400f7d83 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -8,13 +8,18 @@ namespace Magento\GraphQl\Catalog; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\DataObject; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\ObjectManager; +/** + * Test loading of category tree + */ class CategoryTest extends GraphQlAbstract { /** @@ -22,13 +27,18 @@ class CategoryTest extends GraphQlAbstract */ private $objectManager; + /** + * @var CategoryRepository + */ + private $categoryRepository; + protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->get(CategoryRepository::class); } /** - * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Catalog/_files/categories.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -70,21 +80,12 @@ public function testCategoriesTree() } } QUERY; - - // get customer ID token - /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ - $customerTokenService = $this->objectManager->create( - \Magento\Integration\Api\CustomerTokenServiceInterface::class - ); - $customerToken = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); - - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - $response = $this->graphQlQuery($query, [], '', $headerMap); + $response = $this->graphQlQuery($query); $responseDataObject = new DataObject($response); //Some sort of smoke testing self::assertEquals( - 'Ololo', - $responseDataObject->getData('category/children/7/children/1/description') + 'Its a description of Test Category 1.2', + $responseDataObject->getData('category/children/0/children/1/description') ); self::assertEquals( 'default-category', @@ -99,19 +100,89 @@ public function testCategoriesTree() $responseDataObject->getData('category/children/0/default_sort_by') ); self::assertCount( - 8, + 7, $responseDataObject->getData('category/children') ); self::assertCount( 2, - $responseDataObject->getData('category/children/7/children') + $responseDataObject->getData('category/children/0/children') ); self::assertEquals( - 5, - $responseDataObject->getData('category/children/7/children/1/children/0/id') + 13, + $responseDataObject->getData('category/children/0/children/1/id') ); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testCategoriesTreeWithDisabledCategory() + { + $category = $this->categoryRepository->get(3); + $category->setIsActive(false); + $this->categoryRepository->save($category); + + $rootCategoryId = 2; + $query = <<<QUERY +{ + category(id: {$rootCategoryId}) { + id + name + level + description + children { + id + name + productImagePreview: products(pageSize: 1) { + items { + id + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('category', $response); + $this->assertArrayHasKey('children', $response['category']); + $this->assertSame(6, count($response['category']['children'])); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testGetCategoryById() + { + $categoryId = 13; + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + name + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertEquals('Category 1.2', $response['category']['name']); + self::assertEquals(13, $response['category']['id']); + } + + public function testNonExistentCategoryWithProductCount() + { + $query = <<<QUERY +{ + category(id: 99) { + product_count + } +} +QUERY; + + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage('GraphQL response contains errors: Category doesn\'t exist'); + $this->graphQlQuery($query); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -259,17 +330,14 @@ public function testCategoryProducts() } } QUERY; - $response = $this->graphQlQuery($query); $this->assertArrayHasKey('products', $response['category']); $this->assertArrayHasKey('total_count', $response['category']['products']); $this->assertGreaterThanOrEqual(1, $response['category']['products']['total_count']); $this->assertEquals(1, $response['category']['products']['page_info']['current_page']); $this->assertEquals(20, $response['category']['products']['page_info']['page_size']); - $this->assertArrayHasKey('sku', $response['category']['products']['items'][0]); $firstProductSku = $response['category']['products']['items'][0]['sku']; - /** * @var ProductRepositoryInterface $productRepository */ @@ -279,7 +347,6 @@ public function testCategoryProducts() $this->assertAttributes($response['category']['products']['items'][0]); $this->assertWebsites($firstProduct, $response['category']['products']['items'][0]['websites']); } - /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php */ @@ -291,9 +358,7 @@ public function testAnchorCategory() /** @var CategoryInterface $category */ $category = $categoryCollection->getFirstItem(); $categoryId = $category->getId(); - $this->assertNotEmpty($categoryId, "Preconditions failed: category is not available."); - $query = <<<QUERY { category(id: {$categoryId}) { @@ -307,7 +372,6 @@ public function testAnchorCategory() } } QUERY; - $response = $this->graphQlQuery($query); $expectedResponse = [ 'category' => [ @@ -331,7 +395,6 @@ public function testAnchorCategory() */ private function assertBaseFields($product, $actualResponse) { - $assertionMap = [ ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], @@ -365,7 +428,6 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ]; - $this->assertResponseFields($actualResponse, $assertionMap); } @@ -385,7 +447,6 @@ private function assertWebsites($product, $actualResponse) 'is_default' => true, ] ]; - $this->assertEquals($actualResponse, $assertionMap); } @@ -410,7 +471,6 @@ private function assertAttributes($actualResponse) 'special_from_date', 'special_to_date', ]; - foreach ($eavAttributes as $eavAttribute) { $this->assertArrayHasKey($eavAttribute, $actualResponse); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php index b55c6c1d91460..b957292a3ac28 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php @@ -51,7 +51,6 @@ public function testProductWithBaseImage() */ public function testProductWithoutBaseImage() { - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/239'); $productSku = 'simple'; $query = <<<QUERY { @@ -61,12 +60,25 @@ public function testProductWithoutBaseImage() url label } + small_image { + url + label + } } } } QUERY; $response = $this->graphQlQuery($query); self::assertEquals('Simple Product', $response['products']['items'][0]['image']['label']); + self::assertStringEndsWith( + 'images/product/placeholder/image.jpg', + $response['products']['items'][0]['image']['url'] + ); + self::assertEquals('Simple Product', $response['products']['items'][0]['small_image']['label']); + self::assertStringEndsWith( + 'images/product/placeholder/small_image.jpg', + $response['products']['items'][0]['small_image']['url'] + ); } /** 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 a7c83aba89f0a..e517b22ad09de 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -137,6 +137,26 @@ public function testQueryAllFieldsSimpleProduct() sort_order } } + ... on CustomizableCheckboxOption { + checkbox_option: value { + option_type_id + sku + price + price_type + title + sort_order + } + } + ... on CustomizableMultipleOption { + multiple_option: value { + option_type_id + sku + price + price_type + title + sort_order + } + } ...on CustomizableFileOption { product_sku file_option: value { @@ -736,7 +756,7 @@ private function assertOptions($product, $actualResponse) $values = $option->getValues(); /** @var \Magento\Catalog\Model\Product\Option\Value $value */ $value = current($values); - $findValueKeyName = $option->getType() === 'radio' ? 'radio_option' : 'drop_down_option'; + $findValueKeyName = $option->getType() . '_option'; if ($value->getTitle() === $optionsArray[$findValueKeyName][0]['title']) { $match = true; } @@ -756,7 +776,7 @@ private function assertOptions($product, $actualResponse) ]; if (!empty($option->getValues())) { - $valueKeyName = $option->getType() === 'radio' ? 'radio_option' : 'drop_down_option'; + $valueKeyName = $option->getType() . '_option'; $value = current($optionsArray[$valueKeyName]); /** @var \Magento\Catalog\Model\Product\Option\Value $productValue */ $productValue = current($option->getValues()); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php new file mode 100644 index 0000000000000..99f1dc004c50f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product to cart testcases related to inventory + */ +class AddProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage The requested qty is not available + */ + public function testAddProductIfQuantityIsNotAvailable() + { + $sku = 'simple'; + $qty = 200; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoConfigFixture default cataloginventory/item_options/max_sale_qty 5 + * @expectedException \Exception + * @expectedExceptionMessage The most you may purchase is 5. + */ + public function testAddMoreProductsThatAllowed() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/167'); + + $sku = 'custom-design-simple-product'; + $qty = 7; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Please enter a number greater than 0 in this field. + */ + public function testAddSimpleProductToCartWithNegativeQty() + { + $sku = 'simple'; + $qty = -2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, int $qty) : string + { + return <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php new file mode 100644 index 0000000000000..d22cd14a4ae26 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add configurable product to cart testcases + */ +class AddConfigurableProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductToCart() + { + $variantSku = 'simple_41'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $variantSku, $qty); + $response = $this->graphQlMutation($query); + + $cartItems = $response['addConfigurableProductsToCart']['cart']['items']; + self::assertEquals($qty, $cartItems[0]['qty']); + self::assertEquals($variantSku, $cartItems[0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage The requested qty is not available + */ + public function testAddProductIfQuantityIsNotAvailable() + { + $variantSku = 'simple_41'; + $qty = 200; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $variantSku, $qty); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Framework/Search/_files/product_configurable.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Product that you are trying to add is not available. + */ + public function testAddOutOfStockProduct() + { + $variantSku = 'simple_1010'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $variantSku, $qty); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $variantSku + * @param int $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, string $variantSku, int $qty): string + { + return <<<QUERY +mutation { + addConfigurableProductsToCart( + input: { + cart_id: "{$maskedQuoteId}" + cartItems: [ + { + variant_sku: "{$variantSku}" + data: { + qty: {$qty} + sku: "{$variantSku}" + } + } + ] + } + ) { + cart { + items { + id + qty + product { + name + sku + } + ... on ConfigurableCartItem { + configurable_options { + option_label + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductFrontendLabelAttributeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductFrontendLabelAttributeTest.php new file mode 100644 index 0000000000000..32cd3a9a51dcc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductFrontendLabelAttributeTest.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\ConfigurableProduct; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class ConfigurableProductFrontendLabelAttributeTest + * + * @package Magento\GraphQl\ConfigurableProduct + */ +class ConfigurableProductFrontendLabelAttributeTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php + */ + public function testGetFrontendLabelAttribute() + { + $expectLabelValue = 'Default Store View label'; + $productSku = 'configurable'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + name + ... on ConfigurableProduct{ + configurable_options{ + id + label + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertArrayHasKey(0, $response['products']['items']); + + $product = $response['products']['items'][0]; + $this->assertArrayHasKey('configurable_options', $product); + $this->assertArrayHasKey(0, $product['configurable_options']); + $this->assertArrayHasKey('label', $product['configurable_options'][0]); + + $option = $product['configurable_options'][0]; + $this->assertEquals($expectLabelValue, $option['label']); + } +} 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 735ae7fff646b..da5410384627c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php @@ -204,7 +204,6 @@ public function testQueryConfigurableProductLinks() /** * @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $product = $productRepository->get($productSku, false, null, true); @@ -407,6 +406,7 @@ private function assertConfigurableVariants($actualResponse) $variantArray['product']['price'] ); $configurableOptions = $this->getConfigurableOptions(); + $this->assertEquals(1, count($variantArray['attributes'])); foreach ($variantArray['attributes'] as $attribute) { $hasAssertion = false; foreach ($configurableOptions as $option) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php index f4e96e49a58e6..ab6a1d4b464dd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php @@ -14,6 +14,9 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Test change customer password + */ class ChangeCustomerPasswordTest extends GraphQlAbstract { /** @@ -50,7 +53,7 @@ public function testChangePassword() $query = $this->getChangePassQuery($oldCustomerPassword, $newCustomerPassword); $headerMap = $this->getCustomerAuthHeaders($customerEmail, $oldCustomerPassword); - $response = $this->graphQlQuery($query, [], '', $headerMap); + $response = $this->graphQlMutation($query, [], '', $headerMap); $this->assertEquals($customerEmail, $response['changeCustomerPassword']['email']); try { @@ -69,7 +72,7 @@ public function testChangePassword() public function testChangePasswordIfUserIsNotAuthorizedTest() { $query = $this->getChangePassQuery('currentpassword', 'newpassword'); - $this->graphQlQuery($query); + $this->graphQlMutation($query); } /** @@ -77,7 +80,6 @@ public function testChangePasswordIfUserIsNotAuthorizedTest() */ public function testChangeWeakPassword() { - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/190'); $customerEmail = 'customer@example.com'; $oldCustomerPassword = 'password'; $newCustomerPassword = 'weakpass'; @@ -88,13 +90,13 @@ public function testChangeWeakPassword() $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/Minimum of different classes of characters in password is.*/'); - $this->graphQlQuery($query, [], '', $headerMap); + $this->graphQlMutation($query, [], '', $headerMap); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException \Exception - * @expectedExceptionMessage The password doesn't match this account. Verify the password and try again. + * @expectedExceptionMessage Invalid login or password. */ public function testChangePasswordIfPasswordIsInvalid() { @@ -106,7 +108,7 @@ public function testChangePasswordIfPasswordIsInvalid() $query = $this->getChangePassQuery($incorrectPassword, $newCustomerPassword); $headerMap = $this->getCustomerAuthHeaders($customerEmail, $oldCustomerPassword); - $this->graphQlQuery($query, [], '', $headerMap); + $this->graphQlMutation($query, [], '', $headerMap); } private function getChangePassQuery($currentPassword, $newPassword) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php index 602d969924fbd..891c74ca3c1e2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php @@ -13,6 +13,9 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Integration\Api\CustomerTokenServiceInterface; +/** + * Create customer address tests + */ class CreateCustomerAddressTest extends GraphQlAbstract { /** @@ -117,7 +120,7 @@ public function testCreateCustomerAddress() $userName = 'customer@example.com'; $password = 'password'; - $response = $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('createCustomerAddress', $response); $this->assertArrayHasKey('customer_id', $response['createCustomerAddress']); $this->assertEquals($customerId, $response['createCustomerAddress']['customer_id']); @@ -158,7 +161,7 @@ public function testCreateCustomerAddressIfUserIsNotAuthorized() } } MUTATION; - $this->graphQlQuery($mutation); + $this->graphQlMutation($mutation); } /** @@ -195,7 +198,73 @@ public function testCreateCustomerAddressWithMissingAttribute() $userName = 'customer@example.com'; $password = 'password'; - $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreateCustomerAddressWithRedundantStreetLine() + { + $newAddress = [ + 'region' => [ + 'region' => 'Arizona', + 'region_id' => 4, + 'region_code' => 'AZ' + ], + 'country_id' => 'US', + 'street' => ['Line 1 Street', 'Line 2', 'Line 3'], + 'company' => 'Company name', + 'telephone' => '123456789', + 'fax' => '123123123', + 'postcode' => '7777', + 'city' => 'City Name', + 'firstname' => 'Adam', + 'lastname' => 'Phillis', + 'middlename' => 'A', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'vat_id' => '1', + 'default_shipping' => true, + 'default_billing' => false + ]; + + $mutation + = <<<MUTATION +mutation { + createCustomerAddress(input: { + region: { + region: "{$newAddress['region']['region']}" + region_id: {$newAddress['region']['region_id']} + region_code: "{$newAddress['region']['region_code']}" + } + country_id: {$newAddress['country_id']} + street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}","{$newAddress['street'][2]}"] + company: "{$newAddress['company']}" + telephone: "{$newAddress['telephone']}" + fax: "{$newAddress['fax']}" + postcode: "{$newAddress['postcode']}" + city: "{$newAddress['city']}" + firstname: "{$newAddress['firstname']}" + lastname: "{$newAddress['lastname']}" + middlename: "{$newAddress['middlename']}" + prefix: "{$newAddress['prefix']}" + suffix: "{$newAddress['suffix']}" + vat_id: "{$newAddress['vat_id']}" + default_shipping: true + default_billing: false + }) { + id + } +} +MUTATION; + + $userName = 'customer@example.com'; + $password = 'password'; + + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php new file mode 100644 index 0000000000000..fc51f57a83a76 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -0,0 +1,260 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for create customer functionallity + */ +class CreateCustomerTest extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp() + { + parent::setUp(); + + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertEquals($newFirstname, $response['createCustomer']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomer']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomer']['customer']['email']); + $this->assertEquals(true, $response['createCustomer']['customer']['is_subscribed']); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithoutPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertEquals($newFirstname, $response['createCustomer']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomer']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomer']['customer']['email']); + $this->assertEquals(true, $response['createCustomer']['customer']['is_subscribed']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage "input" value should be specified + */ + public function testCreateCustomerIfInputDataIsEmpty() + { + $query = <<<QUERY +mutation { + createCustomer( + input: { + + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The customer email is missing. Enter and try again. + */ + public function testCreateCustomerIfEmailMissed() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage "Email" is not a valid email address. + */ + public function testCreateCustomerIfEmailIsNotValid() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'email'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Field "test123" is not defined by type CustomerInput. + */ + public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + test123: "123test123" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + public function tearDown() + { + $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/Customer/DeleteCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php index ba0232020298f..bdfd428a78c20 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php @@ -13,6 +13,9 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Integration\Api\CustomerTokenServiceInterface; +/** + * Delete customer address tests + */ class DeleteCustomerAddressTest extends GraphQlAbstract { /** @@ -55,7 +58,7 @@ public function testDeleteCustomerAddress() deleteCustomerAddress(id: {$addressId}) } MUTATION; - $response = $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('deleteCustomerAddress', $response); $this->assertEquals(true, $response['deleteCustomerAddress']); } @@ -73,7 +76,7 @@ public function testDeleteCustomerAddressIfUserIsNotAuthorized() deleteCustomerAddress(id: {$addressId}) } MUTATION; - $this->graphQlQuery($mutation); + $this->graphQlMutation($mutation); } /** @@ -99,7 +102,7 @@ public function testDeleteDefaultShippingCustomerAddress() deleteCustomerAddress(id: {$addressId}) } MUTATION; - $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } /** @@ -125,14 +128,14 @@ public function testDeleteDefaultBillingCustomerAddress() deleteCustomerAddress(id: {$addressId}) } MUTATION; - $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * * @expectedException \Exception - * @expectedExceptionMessage Address id 9999 does not exist. + * @expectedExceptionMessage Could not find a address with ID "9999" */ public function testDeleteNonExistCustomerAddress() { @@ -144,7 +147,7 @@ public function testDeleteNonExistCustomerAddress() deleteCustomerAddress(id: 9999) } MUTATION; - $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php index ae28e23a28bf1..88eaeaa8f9dd5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php @@ -38,7 +38,7 @@ public function testGenerateCustomerValidToken() } MUTATION; - $response = $this->graphQlQuery($mutation); + $response = $this->graphQlMutation($mutation); $this->assertArrayHasKey('generateCustomerToken', $response); $this->assertInternalType('array', $response['generateCustomerToken']); } @@ -66,6 +66,6 @@ public function testGenerateCustomerTokenWithInvalidCredentials() $this->expectException(\Exception::class); $this->expectExceptionMessage('GraphQL response contains errors: The account sign-in' . ' ' . 'was incorrect or your account is disabled temporarily. Please wait and try again later.'); - $this->graphQlQuery($mutation); + $this->graphQlMutation($mutation); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php new file mode 100644 index 0000000000000..37693fbba7fef --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class IsEmailAvailableTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmailNotAvailable() + { + $query = + <<<QUERY +{ + isEmailAvailable(email: "customer@example.com") { + is_email_available + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('isEmailAvailable', $response); + self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']); + self::assertFalse($response['isEmailAvailable']['is_email_available']); + } + + public function testEmailAvailable() + { + $query = + <<<QUERY +{ + isEmailAvailable(email: "customer@example.com") { + is_email_available + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('isEmailAvailable', $response); + self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']); + self::assertTrue($response['isEmailAvailable']['is_email_available']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php index 9bdbf3059eeaf..fc0c02bae5508 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php @@ -36,7 +36,7 @@ public function testRevokeCustomerTokenValidCredentials() $customerToken = $customerTokenService->createCustomerAccessToken($userName, $password); $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - $response = $this->graphQlQuery($query, [], '', $headerMap); + $response = $this->graphQlMutation($query, [], '', $headerMap); $this->assertTrue($response['revokeCustomerToken']['result']); } @@ -53,6 +53,6 @@ public function testRevokeCustomerTokenForGuestCustomer() } } QUERY; - $this->graphQlQuery($query, [], ''); + $this->graphQlMutation($query, [], ''); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php index 191ea1ae6b877..2b54c97cd1e97 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php @@ -12,6 +12,9 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Tests for subscription status + */ class SubscriptionStatusTest extends GraphQlAbstract { /** @@ -88,7 +91,12 @@ public function testChangeSubscriptionStatusTest() } } QUERY; - $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); $this->assertTrue($response['updateCustomer']['customer']['is_subscribed']); } @@ -111,7 +119,7 @@ public function testChangeSubscriptionStatuIfUserIsNotAuthorizedTest() } } QUERY; - $this->graphQlQuery($query); + $this->graphQlMutation($query); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php index 519fe2b1405a0..e7a7eda2897b2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php @@ -14,6 +14,9 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Integration\Api\CustomerTokenServiceInterface; +/** + * Update customer address tests + */ class UpdateCustomerAddressTest extends GraphQlAbstract { /** @@ -128,7 +131,7 @@ public function testUpdateCustomerAddress() } MUTATION; - $response = $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('updateCustomerAddress', $response); $this->assertArrayHasKey('customer_id', $response['updateCustomerAddress']); $this->assertEquals($customerId, $response['updateCustomerAddress']['customer_id']); @@ -158,7 +161,7 @@ public function testUpdateCustomerAddressIfUserIsNotAuthorized() } } MUTATION; - $this->graphQlQuery($mutation); + $this->graphQlMutation($mutation); } /** @@ -187,7 +190,7 @@ public function testUpdateCustomerAddressWithMissingAttribute() } } MUTATION; - $this->graphQlQuery($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); } /** @@ -218,13 +221,12 @@ private function assertCustomerAddressesFields(AddressInterface $address, $actua ]; $this->assertResponseFields($actualResponse, $assertionMap); $this->assertTrue(is_array([$actualResponse['region']]), "region field must be of an array type."); - // https://github.com/magento/graphql-ce/issues/270 -// $assertionRegionMap = [ -// ['response_field' => 'region', 'expected_value' => $address->getRegion()->getRegion()], -// ['response_field' => 'region_code', 'expected_value' => $address->getRegion()->getRegionCode()], -// ['response_field' => 'region_id', 'expected_value' => $address->getRegion()->getRegionId()] -// ]; -// $this->assertResponseFields($actualResponse['region'], $assertionRegionMap); + $assertionRegionMap = [ + ['response_field' => 'region', 'expected_value' => $address->getRegion()->getRegion()], + ['response_field' => 'region_code', 'expected_value' => $address->getRegion()->getRegionCode()], + ['response_field' => 'region_id', 'expected_value' => $address->getRegion()->getRegionId()] + ]; + $this->assertResponseFields($actualResponse['region'], $assertionRegionMap); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index c11c1385f7412..08933f47191b9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -13,6 +13,9 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Tests for update customer + */ class UpdateCustomerTest extends GraphQlAbstract { /** @@ -47,33 +50,62 @@ public function testUpdateCustomer() $currentEmail = 'customer@example.com'; $currentPassword = 'password'; + $newPrefix = 'Dr'; $newFirstname = 'Richard'; + $newMiddlename = 'Riley'; $newLastname = 'Rowe'; + $newSuffix = 'III'; + $newDob = '3/11/1972'; + $newTaxVat = 'GQL1234567'; + $newGender = 2; $newEmail = 'customer_updated@example.com'; $query = <<<QUERY mutation { updateCustomer( input: { + prefix: "{$newPrefix}" firstname: "{$newFirstname}" + middlename: "{$newMiddlename}" lastname: "{$newLastname}" + suffix: "{$newSuffix}" + dob: "{$newDob}" + taxvat: "{$newTaxVat}" email: "{$newEmail}" password: "{$currentPassword}" + gender: {$newGender} } ) { customer { + prefix firstname + middlename lastname + suffix + dob + taxvat email + gender } } } QUERY; - $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + $this->assertEquals($newPrefix, $response['updateCustomer']['customer']['prefix']); $this->assertEquals($newFirstname, $response['updateCustomer']['customer']['firstname']); + $this->assertEquals($newMiddlename, $response['updateCustomer']['customer']['middlename']); $this->assertEquals($newLastname, $response['updateCustomer']['customer']['lastname']); + $this->assertEquals($newSuffix, $response['updateCustomer']['customer']['suffix']); + $this->assertEquals($newDob, $response['updateCustomer']['customer']['dob']); + $this->assertEquals($newTaxVat, $response['updateCustomer']['customer']['taxvat']); $this->assertEquals($newEmail, $response['updateCustomer']['customer']['email']); + $this->assertEquals($newGender, $response['updateCustomer']['customer']['gender']); } /** @@ -99,7 +131,7 @@ public function testUpdateCustomerIfInputDataIsEmpty() } } QUERY; - $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } /** @@ -123,7 +155,7 @@ public function testUpdateCustomerIfUserIsNotAuthorized() } } QUERY; - $this->graphQlQuery($query); + $this->graphQlMutation($query); } /** @@ -152,7 +184,7 @@ public function testUpdateCustomerIfAccountIsLocked() } } QUERY; - $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } /** @@ -179,13 +211,13 @@ public function testUpdateEmailIfPasswordIsMissed() } } QUERY; - $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @expectedException \Exception - * @expectedExceptionMessage The password doesn't match this account. Verify the password and try again. + * @expectedExceptionMessage Invalid login or password. */ public function testUpdateEmailIfPasswordIsInvalid() { @@ -208,7 +240,7 @@ public function testUpdateEmailIfPasswordIsInvalid() } } QUERY; - $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } /** @@ -236,7 +268,7 @@ public function testUpdateEmailIfEmailAlreadyExists() } } QUERY; - $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php new file mode 100644 index 0000000000000..c42ce4c46fa29 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's Coutries query + */ +class CountriesTest extends GraphQlAbstract +{ + public function testGetCountries() + { + $query = <<<QUERY +query { + countries { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $this->assertArrayHasKey('countries', $result); + $this->assertArrayHasKey('id', $result['countries'][0]); + $this->assertArrayHasKey('two_letter_abbreviation', $result['countries'][0]); + $this->assertArrayHasKey('three_letter_abbreviation', $result['countries'][0]); + $this->assertArrayHasKey('full_name_locale', $result['countries'][0]); + $this->assertArrayHasKey('full_name_english', $result['countries'][0]); + $this->assertArrayHasKey('available_regions', $result['countries'][0]); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php new file mode 100644 index 0000000000000..dda5ef342247d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's Coutries query + */ +class CountryTest extends GraphQlAbstract +{ + public function testGetCountry() + { + $query = <<<QUERY +query { + country(id: "US") { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $this->assertArrayHasKey('country', $result); + $this->assertArrayHasKey('id', $result['country']); + $this->assertArrayHasKey('two_letter_abbreviation', $result['country']); + $this->assertArrayHasKey('three_letter_abbreviation', $result['country']); + $this->assertArrayHasKey('full_name_locale', $result['country']); + $this->assertArrayHasKey('full_name_english', $result['country']); + $this->assertArrayHasKey('available_regions', $result['country']); + $this->assertArrayHasKey('id', $result['country']['available_regions'][0]); + $this->assertArrayHasKey('code', $result['country']['available_regions'][0]); + $this->assertArrayHasKey('name', $result['country']['available_regions'][0]); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: The country isn't available. + */ + public function testGetCountryNotFoundException() + { + $query = <<<QUERY +query { + country(id: "BLAH") { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + + $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php new file mode 100644 index 0000000000000..ad5d71cb08605 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's Currency query + */ +class CurrencyTest extends GraphQlAbstract +{ + public function testGetCurrency() + { + $query = <<<QUERY +query { + currency { + base_currency_code + base_currency_symbol + default_display_currency_code + default_display_currency_symbol + available_currency_codes + exchange_rates { + currency_to + rate + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $this->assertArrayHasKey('currency', $result); + $this->assertArrayHasKey('base_currency_code', $result['currency']); + $this->assertArrayHasKey('base_currency_symbol', $result['currency']); + $this->assertArrayHasKey('default_display_currency_code', $result['currency']); + $this->assertArrayHasKey('default_display_currency_symbol', $result['currency']); + $this->assertArrayHasKey('available_currency_codes', $result['currency']); + $this->assertArrayHasKey('exchange_rates', $result['currency']); + } +} 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 352947714360a..e784061d5562f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php @@ -393,7 +393,8 @@ public function testQueryComplexityIsLimited() QUERY; self::expectExceptionMessageRegExp('/Max query complexity should be 300 but got 302/'); - $this->graphQlQuery($query); + //Use POST request because request uri is too large for some servers + $this->graphQlMutation($query); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php index 85b4c62c41945..60acb3a7a4d44 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php @@ -12,10 +12,10 @@ class IntrospectionQueryTest extends GraphQlAbstract { /** - * Tests that Introspection is disabled when not in developer mode + * Tests that Introspection is allowed by default * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testIntrospectionQueryWithFieldArgs() + public function testIntrospectionQuery() { $query = <<<QUERY @@ -54,11 +54,6 @@ public function testIntrospectionQueryWithFieldArgs() } QUERY; - $this->expectException(\Exception::class); - $this->expectExceptionMessage( - 'GraphQL response contains errors: GraphQL introspection is not allowed, but ' . - 'the query contained __schema or __type' - ); - $this->graphQlQuery($query); + $this->assertArrayHasKey('__schema', $this->graphQlQuery($query)); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php new file mode 100644 index 0000000000000..f33ccce82fcb7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product with custom options to cart testcases + */ +class AddSimpleProductWithCustomOptionsToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionsRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $sku = 'simple'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + + /* Generate customizable options fragment for GraphQl request */ + $queryCustomizableOptions = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + }, + customizable_options: $queryCustomizableOptions + } + ] + } + ) { + cart { + items { + ... on SimpleCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addSimpleProductsToCart']['cart']); + self::assertCount(1, $response['addSimpleProductsToCart']['cart']); + + $customizableOptionsOutput = $response['addSimpleProductsToCart']['cart']['items'][0]['customizable_options']; + $assignedOptionsCount = count($customOptionsValues); + for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { + self::assertEquals( + $customOptionsValues[$counter]['value'], + $customizableOptionsOutput[$counter]['values'][0]['value'] + ); + } + } + + /** + * Test adding a simple product with empty values for required options + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithNoRequiredOptionsSet() + { + $sku = 'simple'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + ... on SimpleCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + self::expectExceptionMessage( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ); + + $this->graphQlMutation($query); + } + + /** + * Generate an array with test values for customizable options + * based on the option type + * + * @param string $sku + * @return array + */ + private function getCustomOptionsValuesForQuery(string $sku): array + { + $customOptions = $this->productCustomOptionsRepository->getList($sku); + $customOptionsValues = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + if ($optionType == 'field' || $optionType == 'area') { + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => 'test' + ]; + } elseif ($optionType == 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => reset($optionSelectValues)->getOptionTypeId() + ]; + } + } + + return $customOptionsValues; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php new file mode 100644 index 0000000000000..ffd52bcf7fb15 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Add virtual product with custom options to cart testcases + */ +class AddVirtualProductWithCustomOptionsToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionsRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + } + + /** + * Test adding a virtual product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddVirtualProductWithOptions() + { + $sku = 'virtual'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + + /* Generate customizable options fragment for GraphQl request */ + $queryCustomizableOptions = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + + $query = <<<QUERY +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + }, + customizable_options: $queryCustomizableOptions + } + ] + } + ) { + cart { + items { + ... on VirtualCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addVirtualProductsToCart']['cart']); + self::assertCount(1, $response['addVirtualProductsToCart']['cart']); + + $customizableOptionsOutput = $response['addVirtualProductsToCart']['cart']['items'][0]['customizable_options']; + $assignedOptionsCount = count($customOptionsValues); + for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { + self::assertEquals( + $customOptionsValues[$counter]['value'], + $customizableOptionsOutput[$counter]['values'][0]['value'] + ); + } + } + + /** + * Test adding a virtual product with empty values for required options + * + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddVirtualProductWithNoRequiredOptionsSet() + { + $sku = 'virtual'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = <<<QUERY +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + ... on VirtualCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + self::expectExceptionMessage( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ); + + $this->graphQlMutation($query); + } + + /** + * Generate an array with test values for customizable options + * based on the option type + * + * @param string $sku + * @return array + */ + private function getCustomOptionsValuesForQuery(string $sku): array + { + $customOptions = $this->productCustomOptionsRepository->getList($sku); + $customOptionsValues = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + if ($optionType == 'field' || $optionType == 'area') { + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => 'test' + ]; + } elseif ($optionType == 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => reset($optionSelectValues)->getOptionTypeId() + ]; + } + } + + return $customOptionsValues; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php deleted file mode 100644 index 1f8ad06a9f8ed..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CouponTest.php +++ /dev/null @@ -1,225 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Quote; - -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\TestCase\GraphQlAbstract; - -/** - * Test for adding/removing shopping cart coupon codes - */ -class CouponTest extends GraphQlAbstract -{ - /** - * @var QuoteResource - */ - private $quoteResource; - - /** - * @var Quote - */ - private $quote; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; - - protected function setUp() - { - $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->create(QuoteResource::class); - $this->quote = $objectManager->create(Quote::class); - $this->quoteIdToMaskedId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php - */ - public function testApplyCouponToGuestCartWithItems() - { - $couponCode = '2?ds5!2d'; - - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey("applyCouponToCart", $response); - self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php - */ - public function testApplyCouponTwice() - { - $couponCode = '2?ds5!2d'; - - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey("applyCouponToCart", $response); - self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); - - self::expectExceptionMessage('A coupon is already applied to the cart. Please remove it to apply another'); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php - * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php - */ - public function testApplyCouponToCartWithNoItems() - { - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/191'); - $couponCode = '2?ds5!2d'; - - $this->quoteResource->load($this->quote, 'test_order_1', 'reserved_order_id'); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); - - self::expectExceptionMessageRegExp('/Cart doesn\'t contain products/'); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php - * @magentoApiDataFixture Magento/Customer/_files/customer.php - */ - public function testGuestCustomerAttemptToChangeCustomerCart() - { - $couponCode = '2?ds5!2d'; - - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $this->quote->setCustomerId(1); - $this->quoteResource->save($this->quote); - $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); - - self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php - */ - public function testRemoveCoupon() - { - $couponCode = '2?ds5!2d'; - - /* Apply coupon to the quote */ - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $query = $this->prepareAddCouponRequestQuery($maskedQuoteId, $couponCode); - $this->graphQlQuery($query); - - /* Remove coupon from quote */ - $query = $this->prepareRemoveCouponRequestQuery($maskedQuoteId); - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey('removeCouponFromCart', $response); - self::assertSame('', $response['removeCouponFromCart']['cart']['applied_coupon']['code']); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php - * @magentoApiDataFixture Magento/Customer/_files/customer.php - */ - public function testRemoveCouponFromCustomerCartByGuest() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $this->quote->setCustomerId(1); - $this->quoteResource->save($this->quote); - $query = $this->prepareRemoveCouponRequestQuery($maskedQuoteId); - - self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); - $this->graphQlQuery($query); - } - - /** - * @param string $maskedQuoteId - * @param string $couponCode - * @return string - */ - private function prepareAddCouponRequestQuery(string $maskedQuoteId, string $couponCode): string - { - return <<<QUERY -mutation { - applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { - cart { - applied_coupon { - code - } - } - } -} -QUERY; - } - - /** - * @param string $maskedQuoteId - * @return string - */ - private function prepareRemoveCouponRequestQuery(string $maskedQuoteId): string - { - return <<<QUERY -mutation { - removeCouponFromCart(input: {cart_id: "$maskedQuoteId"}) { - cart { - applied_coupon { - code - } - } - } -} - -QUERY; - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CreateEmptyCartTest.php deleted file mode 100644 index 6e819b523ec82..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CreateEmptyCartTest.php +++ /dev/null @@ -1,91 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Quote; - -use Magento\Quote\Api\Data\CartInterface; -use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\Quote\Model\QuoteIdMask; -use Magento\Quote\Api\GuestCartRepositoryInterface; - -/** - * Test for empty cart creation mutation - */ -class CreateEmptyCartTest extends GraphQlAbstract -{ - /** - * @var QuoteIdMask - */ - private $quoteIdMask; - - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var GuestCartRepositoryInterface - */ - private $guestCartRepository; - - protected function setUp() - { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->quoteIdMask = $this->objectManager->create(QuoteIdMask::class); - $this->guestCartRepository = $this->objectManager->create(GuestCartRepositoryInterface::class); - } - - public function testCreateEmptyCartForGuest() - { - $query = <<<QUERY -mutation { - createEmptyCart -} -QUERY; - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey('createEmptyCart', $response); - - $maskedCartId = $response['createEmptyCart']; - /** @var CartInterface $guestCart */ - $guestCart = $this->guestCartRepository->get($maskedCartId); - - self::assertNotNull($guestCart->getId()); - self::assertNull($guestCart->getCustomer()->getId()); - } - - /** - * @magentoApiDataFixture Magento/Customer/_files/customer.php - */ - public function testCreateEmptyCartForRegisteredCustomer() - { - $query = <<<QUERY -mutation { - createEmptyCart -} -QUERY; - - /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ - $customerTokenService = $this->objectManager->create( - \Magento\Integration\Api\CustomerTokenServiceInterface::class - ); - $customerToken = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - - $response = $this->graphQlQuery($query, [], '', $headerMap); - - self::assertArrayHasKey('createEmptyCart', $response); - - $maskedCartId = $response['createEmptyCart']; - /* guestCartRepository is used for registered customer to get the cart hash */ - $guestCart = $this->guestCartRepository->get($maskedCartId); - - self::assertNotNull($guestCart->getId()); - self::assertEquals(1, $guestCart->getCustomer()->getId()); - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php new file mode 100644 index 0000000000000..73b3e39721866 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test adding simple product to Cart + */ +class AddSimpleProductToCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddSimpleProductToCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][0]['qty']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddProductToNonExistentCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "simple_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddSimpleProductToGuestCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddSimpleProductToAnotherCustomerCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, int $qty): string + { + return <<<QUERY +mutation { + addSimpleProductsToCart(input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: {$qty} + sku: "{$sku}" + } + } + ] + }) { + cart { + items { + id + qty + product { + sku + } + } + } + } +} +QUERY; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php new file mode 100644 index 0000000000000..4ec25bb030079 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test adding virtual product to Cart + */ +class AddVirtualProductToCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddVirtualProductToCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['addVirtualProductsToCart']); + self::assertEquals($qty, $response['addVirtualProductsToCart']['cart']['items'][0]['qty']); + self::assertEquals($sku, $response['addVirtualProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddVirtualToNonExistentCart() + { + $sku = 'virtual_product'; + $qty = 2; + $nonExistentMaskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($nonExistentMaskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "virtual_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddVirtualProductToGuestCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddVirtualProductToAnotherCustomerCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, int $qty): string + { + return <<<QUERY +mutation { + addVirtualProductsToCart(input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + qty: {$qty} + sku: "{$sku}" + } + } + ] + }) { + cart { + items { + id + qty + product { + sku + } + } + } + } +} +QUERY; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php new file mode 100644 index 0000000000000..5a2221a184294 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php @@ -0,0 +1,283 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test Apply Coupon to Cart functionality for customer + */ +class ApplyCouponToCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testApplyCouponToCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage A coupon is already applied to the cart. Please remove it to apply another + */ + public function testApplyCouponTwice() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey("applyCouponToCart", $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products. + */ + public function testApplyCouponToCartWithoutItems() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponToGuestCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + */ + public function testApplyCouponToAnotherCustomerCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer_two@example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyNonExistentCouponToCart() + { + $couponCode = 'non_existent_coupon_code'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponToNonExistentCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('Could not find a cart with ID "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_coupon_expired.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyExpiredCoupon() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/574'); + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Products in cart don't fit to the coupon + * + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyCouponWhichIsNotApplicable() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + */ + public function testApplyCouponWithMissedRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {{$input}}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'coupon_code: "test"', + 'Required parameter "cart_id" is missing' + ], + 'missed_coupon_code' => [ + 'cart_id: "test"', + 'Required parameter "coupon_code" is missing' + ], + ]; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function getQuery(string $maskedQuoteId, string $couponCode): string + { + return <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CartTotalsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CartTotalsTest.php new file mode 100644 index 0000000000000..bb8acfce629ff --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CartTotalsTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test getting cart totals for registered customer + */ +class CartTotalsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetCartTotalsWithTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('prices', $response['cart']); + $pricesResponse = $response['cart']['prices']; + self::assertEquals(21.5, $pricesResponse['grand_total']['value']); + self::assertEquals(21.5, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + + $appliedTaxesResponse = $pricesResponse['applied_taxes']; + + self::assertEquals('US-TEST-*-Rate-1', $appliedTaxesResponse[0]['label']); + self::assertEquals(1.5, $appliedTaxesResponse[0]['amount']['value']); + self::assertEquals('USD', $appliedTaxesResponse[0]['amount']['currency']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsWithNoTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * The totals calculation is based on quote address. + * But the totals should be calculated even if no address is set + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetCartTotalsWithNoAddressSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * Generates GraphQl query for retrieving cart totals + * + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + prices { + grand_total { + value, + currency + } + subtotal_including_tax { + value + currency + } + subtotal_excluding_tax { + value + currency + } + subtotal_with_discount_excluding_tax { + value + currency + } + applied_taxes { + label + amount { + value + currency + } + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php new file mode 100644 index 0000000000000..8592a986c5dce --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php @@ -0,0 +1,530 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * End to checkout tests for customer + */ +class CheckoutEndToEndTest extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var array + */ + private $headers = []; + + protected function setUp() + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->registry = $objectManager->get(Registry::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php + */ + public function testCheckoutWorkflow() + { + $qty = 2; + + $this->createCustomer(); + $token = $this->loginCustomer(); + $this->headers = ['Authorization' => 'Bearer ' . $token]; + + $sku = $this->findProduct(); + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $qty, $sku); + + $this->setBillingAddress($cartId); + $shippingAddress = $this->setShippingAddress($cartId); + + $shippingMethod = current($shippingAddress['available_shipping_methods']); + $paymentMethod = $this->setShippingMethod($cartId, $shippingAddress['address_id'], $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderId = $this->placeOrder($cartId); + $this->checkOrderInHistory($orderId); + } + + /** + * @return void + */ + private function createCustomer(): void + { + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "endto" + lastname: "endtester" + email: "customer@example.com" + password: "123123Qa" + } + ) { + customer { + id + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @return string + */ + private function loginCustomer(): string + { + $query = <<<QUERY +mutation { + generateCustomerToken( + email: "customer@example.com" + password: "123123Qa" + ) { + token + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('generateCustomerToken', $response); + self::assertArrayHasKey('token', $response['generateCustomerToken']); + self::assertNotEmpty($response['generateCustomerToken']['token']); + + return $response['generateCustomerToken']['token']; + } + + /** + * @return string + */ + private function findProduct(): string + { + $query = <<<QUERY +{ + products ( + filter: { + sku: { + like:"simple%" + } + } + pageSize: 1 + currentPage: 1 + ) { + items { + sku + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + + $product = current($response['products']['items']); + self::assertArrayHasKey('sku', $product); + self::assertNotEmpty($product['sku']); + + return $product['sku']; + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + return $response['createEmptyCart']; + } + + /** + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cartItems: [ + { + data: { + qty: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart { + items { + qty + product { + sku + } + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->headers); + } + + /** + * @param string $cartId + * @param array $auth + * @return array + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + postcode: "887766" + telephone: "88776655" + region: "TX" + country_code: "US" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + address_type + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->headers); + } + + /** + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "TX" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + address_id + available_shipping_methods { + carrier_code + method_code + amount + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('setShippingAddressesOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingAddressesOnCart']['cart']); + self::assertCount(1, $response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('address_id', $shippingAddress); + self::assertNotEmpty($shippingAddress['address_id']); + self::assertArrayHasKey('available_shipping_methods', $shippingAddress); + self::assertCount(1, $shippingAddress['available_shipping_methods']); + + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + self::assertArrayHasKey('carrier_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['carrier_code']); + + self::assertArrayHasKey('method_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['method_code']); + + self::assertArrayHasKey('amount', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['amount']); + + return $shippingAddress; + } + + /** + * @param string $cartId + * @param int $addressId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, int $addressId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + cart_address_id: {$addressId} + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('available_payment_methods', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + self::assertArrayHasKey('code', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['code']); + self::assertArrayHasKey('title', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['title']); + + return $availablePaymentMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->headers); + } + + /** + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_id + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_id']); + + return $response['placeOrder']['order']['order_id']; + } + + /** + * @param string $orderId + * @return void + */ + private function checkOrderInHistory(string $orderId): void + { + $query = <<<QUERY +{ + customerOrders { + items { + increment_id + grand_total + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->headers); + self::assertArrayHasKey('customerOrders', $response); + self::assertArrayHasKey('items', $response['customerOrders']); + self::assertCount(1, $response['customerOrders']['items']); + + $order = current($response['customerOrders']['items']); + self::assertArrayHasKey('increment_id', $order); + self::assertEquals($orderId, $order['increment_id']); + + self::assertArrayHasKey('grand_total', $order); + } + + public function tearDown() + { + $this->deleteCustomer(); + $this->deleteQuote(); + $this->deleteOrder(); + parent::tearDown(); + } + + /** + * @return void + */ + private function deleteCustomer(): void + { + $email = 'customer@example.com'; + try { + $customer = $this->customerRepository->get($email); + } 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); + } + + /** + * @return void + */ + private function deleteQuote(): void + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + } + + /** + * @return void + */ + private function deleteOrder() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php new file mode 100644 index 0000000000000..fbd0cf19740d7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Quote\Api\GuestCartRepositoryInterface; + +/** + * Test for empty cart creation mutation for customer + */ +class CreateEmptyCartTest extends GraphQlAbstract +{ + /** + * @var GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCart() + { + $query = $this->getQuery(); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + + self::assertNotNull($guestCart->getId()); + self::assertEquals(1, $guestCart->getCustomer()->getId()); + self::assertEquals('default', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCartWithNotDefaultStore() + { + $query = $this->getQuery(); + + $headerMap = $this->getHeaderMapWithCustomerToken(); + $headerMap['Store'] = 'fixture_second_store'; + $response = $this->graphQlMutation($query, [], '', $headerMap); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + /* guestCartRepository is used for registered customer to get the cart hash */ + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + + self::assertNotNull($guestCart->getId()); + self::assertEquals(1, $guestCart->getCustomer()->getId()); + self::assertEquals('fixture_second_store', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCartWithPredefinedCartId() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertEquals($predefinedCartId, $response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + self::assertNotNull($guestCart->getId()); + self::assertEquals(1, $guestCart->getCustomer()->getId()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart with ID "572cda51902b5b517c0e1a2b2fd004b4" already exists. + */ + public function testCreateEmptyCartIfPredefinedCartIdAlreadyExists() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart ID length should to be 32 symbols. + */ + public function testCreateEmptyCartWithWrongPredefinedCartId() + { + $predefinedCartId = '572'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + } + + /** + * @return string + */ + private function getQuery(): string + { + return <<<QUERY +mutation { + createEmptyCart +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMapWithCustomerToken( + string $username = 'customer@example.com', + string $password = 'password' + ): array { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + public function tearDown() + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php new file mode 100644 index 0000000000000..673d496302662 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get available payment methods + */ +class GetAvailablePaymentMethodsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); + self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethodsFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethodsFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php + */ + public function testGetAvailablePaymentMethodsIfPaymentsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEmpty($response['cart']['available_payment_methods']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailablePaymentMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + available_payment_methods { + code + title + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php new file mode 100644 index 0000000000000..2b647f61c4c63 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.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\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get available shipping methods + */ +class GetAvailableShippingMethodsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * Test case: get available shipping methods from current customer quote + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId), [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + self::assertArrayHasKey('available_shipping_methods', $response['cart']['shipping_addresses'][0]); + self::assertCount(1, $response['cart']['shipping_addresses'][0]['available_shipping_methods']); + + $expectedAddressData = [ + 'amount' => 10, + 'base_amount' => 10, + 'carrier_code' => 'flatrate', + 'carrier_title' => 'Flat Rate', + 'error_message' => '', + 'method_code' => 'flatrate', + 'method_title' => 'Fixed', + 'price_incl_tax' => 10, + 'price_excl_tax' => 10, + ]; + self::assertEquals( + $expectedAddressData, + $response['cart']['shipping_addresses'][0]['available_shipping_methods'][0] + ); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethodsFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * Test case: get available shipping methods from quote of another customer + * + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethodsFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * Test case: get available shipping methods when all shipping methods are disabled + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php + */ + public function testGetAvailableShippingMethodsIfShippingMethodsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId), [], '', $this->getHeaderMap()); + + self::assertEmpty($response['cart']['shipping_addresses'][0]['available_shipping_methods']); + } + + /** + * Test case: get available shipping methods from non-existent cart + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailableShippingMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +query { + cart (cart_id: "{$maskedQuoteId}") { + shipping_addresses { + available_shipping_methods { + amount + base_amount + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax + price_incl_tax + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartEmailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartEmailTest.php new file mode 100644 index 0000000000000..951fe08db5e3d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartEmailTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting email from cart + */ +class GetCartEmailTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetCartEmail() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('email', $response['cart']); + $this->assertEquals('customer@example.com', $response['cart']['email']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetCartEmailFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + */ + public function testGetEmailFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetEmailFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id:"$maskedQuoteId") { + email + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php new file mode 100644 index 0000000000000..19b72b9e3ca4c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertCount(2, $response['cart']['items']); + + self::assertNotEmpty($response['cart']['items'][0]['id']); + self::assertEquals(2, $response['cart']['items'][0]['qty']); + self::assertEquals('simple_product', $response['cart']['items'][0]['product']['sku']); + + self::assertNotEmpty($response['cart']['items'][1]['id']); + self::assertEquals(2, $response['cart']['items'][1]['qty']); + self::assertEquals('virtual-product', $response['cart']['items'][1]['product']['sku']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testGetGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current user does not have an active cart. + */ + public function testGetInactiveCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_customer_not_default_store.php + */ + public function testGetCartWithNotDefaultStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'fixture_second_store'; + + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Wrong store code specified for cart + */ + public function testGetCartWithWrongStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'fixture_second_store'; + + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_customer_not_default_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Store code not_existing_store does not exist + */ + public function testGetCartWithNotExistingStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'not_existing_store'; + + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "{$maskedQuoteId}") { + items { + id + qty + product { + sku + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php new file mode 100644 index 0000000000000..ba169d7a5bbc9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php @@ -0,0 +1,183 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get selected shipping method + */ +class GetSelectedShippingMethodTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethodFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethodFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); + self::assertNull($shippingAddress['selected_shipping_method']['method_code']); + self::assertNull($shippingAddress['selected_shipping_method']['label']); + self::assertNull($shippingAddress['selected_shipping_method']['amount']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetGetSelectedShippingMethodOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + amount + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php new file mode 100644 index 0000000000000..1ba94346073db --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php @@ -0,0 +1,219 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get specified billing address + */ +class GetSpecifiedBillingAddressTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => 'CompanyName', + 'street' => [ + 'Green str, 67' + ], + 'city' => 'CityM', + 'region' => [ + 'code' => 'AL', + 'label' => 'Alabama', + ], + 'postcode' => '75477', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + 'telephone' => '3468676', + 'address_type' => 'BILLING', + 'customer_notes' => null, + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGeSpecifiedBillingAddressIfBillingAddressIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => null, + 'lastname' => null, + 'company' => null, + 'street' => [ + '' + ], + 'city' => null, + 'region' => [ + 'code' => null, + 'label' => null, + ], + 'postcode' => null, + 'country' => [ + 'code' => null, + 'label' => null, + ], + 'telephone' => null, + 'address_type' => 'BILLING', + 'customer_notes' => null, + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGeSpecifiedBillingAddressOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddressFromAnotherGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($this->getQuery($maskedQuoteId), [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddressFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery( + $this->getQuery($maskedQuoteId), + [], + '', + $this->getHeaderMap('customer2@search.example.com') + ); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + billing_address { + firstname + lastname + company + street + city + region + { + code + label + } + postcode + country + { + code + label + } + telephone + address_type + customer_notes + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php new file mode 100644 index 0000000000000..47d0d661fb33c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php @@ -0,0 +1,295 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Registry; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for placing an order for customer + */ +class PlaceOrderTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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 testPlaceOrder() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testPlaceOrderWithNoItemsInCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: A server error stopped your order from being placed. ' . + 'Please try to place your order again' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testPlaceOrderWithNoShippingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: Some addresses can\'t be used due to the configurations for specific countries' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testPlaceOrderWithNoShippingMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: The shipping method is missing. Select the shipping method and try again' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoBillingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp( + '/Unable to place order: Please check the billing address information*/' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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 + */ + public function testPlaceOrderWithNoPaymentMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Enter a valid payment method and try again'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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/Catalog/_files/set_simple_product_out_of_stock.php + */ + public function testPlaceOrderWithOutOfStockProduct() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Some of the products are out of stock'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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 testPlaceOrderOfGuestCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp('/The current user cannot perform operations on cart*/'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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 testPlaceOrderOfAnotherCustomerCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp('/The current user cannot perform operations on cart*/'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { + order { + order_id + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @inheritdoc + */ + public function tearDown() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php new file mode 100644 index 0000000000000..ce1c85417b165 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Check removing of the coupon from customer cart + */ +class RemoveCouponFromCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveCouponFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products + */ + public function testRemoveCouponFromEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveCouponFromCartIfCouponWasNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + removeCouponFromCart(input: {cart_id: "$maskedQuoteId"}) { + cart { + applied_coupon { + code + } + } + } +} + +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php new file mode 100644 index 0000000000000..39803f8d58447 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for removeItemFromCartTest mutation + */ +class RemoveItemFromCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteItemIdByReservedQuoteIdAndSku + */ + private $getQuoteItemIdByReservedQuoteIdAndSku; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteItemIdByReservedQuoteIdAndSku = $objectManager->get( + GetQuoteItemIdByReservedQuoteIdAndSku::class + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $itemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $query = $this->getQuery($maskedQuoteId, $itemId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('removeItemFromCart', $response); + $this->assertArrayHasKey('cart', $response['removeItemFromCart']); + $this->assertCount(0, $response['removeItemFromCart']['cart']['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveItemFromNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveNonExistentItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Cart doesn't contain the {$notExistentItemId} item."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + removeItemFromCart( + input: { + {$input} + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'cart_item_id: 1', + 'Required parameter "cart_id" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_id: "test"', + 'Required parameter "cart_item_id" is missing.' + ], + ]; + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + */ + public function testRemoveItemIfItemIsNotBelongToCart() + { + $firstQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $secondQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute( + 'test_order_with_virtual_product', + 'virtual-product' + ); + + $this->expectExceptionMessage("Cart doesn't contain the {$secondQuoteItemId} item."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromGuestCart() + { + $guestQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $guestQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$guestQuoteMaskedId\"" + ); + + $query = $this->getQuery($guestQuoteMaskedId, $guestQuoteItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromAnotherCustomerCart() + { + $anotherCustomerQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $anotherCustomerQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute( + 'test_quote', + 'simple_product' + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$anotherCustomerQuoteMaskedId\"" + ); + + $query = $this->getQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId): string + { + return <<<QUERY +mutation { + removeItemFromCart( + input: { + cart_id: "{$maskedQuoteId}" + cart_item_id: {$itemId} + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php new file mode 100644 index 0000000000000..6b15f947a2477 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -0,0 +1,628 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set billing address on cart mutation + */ +class SetBillingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithUseForShippingParameter() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + use_for_shipping: true + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'SHIPPING'); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertSavedBillingAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "100" + */ + public function testSetNotExistedBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 100 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressAndFromAddressBookAtSameTime() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + self::expectExceptionMessage( + 'The billing address cannot contain "customer_address_id" and "address" at the same time.' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetBillingAddressToGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetBillingAddressToAnotherCustomerCart() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer@search.example.com')); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current customer does not have permission to address with ID "1" + */ + public function testSetBillingAddressIfCustomerIsNotOwnerOfAddress() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetBillingAddressOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderSetWithoutRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetBillingAddressWithoutRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + {$input} + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderSetWithoutRequiredParameters(): array + { + return [ + 'missed_billing_address' => [ + 'cart_id: "cart_id_value"', + 'Field SetBillingAddressOnCartInput.billing_address of required type BillingAddressInput!' + . ' was not provided.', + ], + 'missed_cart_id' => [ + 'billing_address: {}', + 'Required parameter "cart_id" is missing' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + } + } + } +} +QUERY; + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $addressResponse + * @param string $addressType + */ + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BILLING'): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => $addressType] + ]; + + $this->assertResponseFields($addressResponse, $assertionMap); + } + + /** + * Verify the all the whitelisted fields for a Address Object + * + * @param array $billingAddressResponse + */ + private function assertSavedBillingAddressFields(array $billingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'John'], + ['response_field' => 'lastname', 'expected_value' => 'Smith'], + ['response_field' => 'company', 'expected_value' => 'CompanyName'], + ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']], + ['response_field' => 'city', 'expected_value' => 'CityM'], + ['response_field' => 'postcode', 'expected_value' => '75477'], + ['response_field' => 'telephone', 'expected_value' => '3468676'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ]; + + $this->assertResponseFields($billingAddressResponse, $assertionMap); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $reservedOrderId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reservedOrderId = 'test_order_with_simple_product_without_address', + int $customerId = 1 + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetGuestEmailOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetGuestEmailOnCartTest.php new file mode 100644 index 0000000000000..a4a84c2f8c740 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetGuestEmailOnCartTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setGuestEmailOnCart mutation + */ +class SetGuestEmailOnCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage The request is not allowed for logged in customers + */ + public function testSetGuestEmailOnCartForLoggedInCustomer() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage The request is not allowed for logged in customers + */ + public function testSetGuestEmailOnGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Returns GraphQl mutation query for setting email address for a guest + * + * @param string $maskedQuoteId + * @param string $email + * @return string + */ + private function getQuery(string $maskedQuoteId, string $email): string + { + return <<<QUERY +mutation { + setGuestEmailOnCart(input: { + cart_id: "$maskedQuoteId" + email: "$email" + }) { + cart { + email + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..20462220ff6f7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteShippingAddressIdByReservedQuoteId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting offline shipping methods on cart + */ +class SetOfflineShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteShippingAddressIdByReservedQuoteId + */ + private $getQuoteShippingAddressIdByReservedQuoteId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteShippingAddressIdByReservedQuoteId = $objectManager->get( + GetQuoteShippingAddressIdByReservedQuoteId::class + ); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/OfflineShipping/_files/tablerates_weight.php + * + * @param string $carrierCode + * @param string $methodCode + * @param float $amount + * @param string $label + * @dataProvider offlineShippingMethodDataProvider + */ + public function testSetOfflineShippingMethod(string $carrierCode, string $methodCode, float $amount, string $label) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + self::assertEquals($amount, $shippingAddress['selected_shipping_method']['amount']); + + self::assertArrayHasKey('label', $shippingAddress['selected_shipping_method']); + self::assertEquals($label, $shippingAddress['selected_shipping_method']['label']); + } + + /** + * @return array + */ + public function offlineShippingMethodDataProvider(): array + { + return [ + 'flatrate_flatrate' => ['flatrate', 'flatrate', 10, 'Flat Rate - Fixed'], + 'tablerate_bestway' => ['tablerate', 'bestway', 10, 'Best Way - Table Rate'], + 'freeshipping_freeshipping' => ['freeshipping', 'freeshipping', 0, 'Free Shipping - Free'], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param int $shippingAddressId + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + int $shippingAddressId + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + cart_address_id: $shippingAddressId + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + amount + label + } + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php new file mode 100644 index 0000000000000..2604ec5f0a0f9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php @@ -0,0 +1,310 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\OfflinePayments\Model\Cashondelivery; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting payment methods on cart by customer + */ +class SetPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetPaymentOnCartWithSimpleProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentOnCartWithSimpleProductAndWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testSetPaymentOnCartWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetNonExistentPaymentMethod() + { + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetPaymentOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetPaymentMethodToGuestCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetPaymentMethodToAnotherCustomerCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @param string $input + * @param string $message + * @throws Exception + * @dataProvider dataProviderSetPaymentMethodWithoutRequiredParameters + */ + public function testSetPaymentMethodWithoutRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + {$input} + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetDisabledPaymentOnCart() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', + 'Required parameter "cart_id" is missing.' + ], + 'missed_payment_method' => [ + 'cart_id: "test"', + 'Required parameter "code" for "payment_method" is missing.' + ], + 'missed_payment_method_code' => [ + 'cart_id: "test", payment_method: {code: ""}', + 'Required parameter "code" for "payment_method" is missing.' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testReSetPayment() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $methodCode = Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE; + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $methodCode + ) : string { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php new file mode 100644 index 0000000000000..6b097e028ffe5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -0,0 +1,604 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set shipping addresses on cart mutation + */ +class SetShippingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + label + code + } + address_type + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. + */ + public function testSetNewShippingAddressOnCartWithVirtualProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertSavedShippingAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "100" + */ + public function testSetNonExistentShippingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 100 + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressAndFromAddressBookAtSameTime() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1, + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + self::expectExceptionMessage( + 'The shipping address cannot contain "customer_address_id" and "address" at the same time.' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current customer does not have permission to address with ID "1" + */ + public function testSetShippingAddressIfCustomerIsNotOwnerOfAddress() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + postcode + } + } + } +} +QUERY; + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @expectedException \Exception + */ + public function testSetShippingAddressToAnotherCustomerCart() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 1); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + postcode + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetNewShippingAddressWithMissedRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$maskedQuoteId}" + shipping_addresses: [ + { + {$input} + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'shipping_addresses' => [ + '', + 'The shipping address must contain either "customer_address_id" or "address".', + ], + 'missed_city' => [ + 'address: { save_in_address_book: false }', + 'Field CartAddressInput.city of required type String! was not provided' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage You cannot specify multiple shipping addresses. + */ + public function testSetMultipleNewShippingAddresses() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + }, + { + address: { + firstname: "test firstname 2" + lastname: "test lastname 2" + company: "test company 2" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + } + } + } +} +QUERY; + + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $shippingAddressResponse + */ + private function assertNewShippingAddressFields(array $shippingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => 'SHIPPING'] + ]; + + $this->assertResponseFields($shippingAddressResponse, $assertionMap); + } + + /** + * Verify the all the whitelisted fields for a Address Object + * + * @param array $shippingAddressResponse + */ + private function assertSavedShippingAddressFields(array $shippingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'John'], + ['response_field' => 'lastname', 'expected_value' => 'Smith'], + ['response_field' => 'company', 'expected_value' => 'CompanyName'], + ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']], + ['response_field' => 'city', 'expected_value' => 'CityM'], + ['response_field' => 'postcode', 'expected_value' => '75477'], + ['response_field' => 'telephone', 'expected_value' => '3468676'] + ]; + + $this->assertResponseFields($shippingAddressResponse, $assertionMap); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $reservedOrderId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reservedOrderId = 'test_order_with_simple_product_without_address', + int $customerId = 1 + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..0fc52443e86b9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -0,0 +1,469 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteShippingAddressIdByReservedQuoteId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting shipping methods on cart for customer + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteShippingAddressIdByReservedQuoteId + */ + private $getQuoteShippingAddressIdByReservedQuoteId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteShippingAddressIdByReservedQuoteId = $objectManager->get( + GetQuoteShippingAddressIdByReservedQuoteId::class + ); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetShippingMethodOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testReSetShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'freeshipping'; + $methodCode = 'freeshipping'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetShippingMethodWithWrongParameters + * @throws Exception + */ + public function testSetShippingMethodWithWrongParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + $input = str_replace(['cart_id_value', 'cart_address_id_value'], [$maskedQuoteId, $quoteAddressId], $input); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + {$input} + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function dataProviderSetShippingMethodWithWrongParameters(): array + { + return [ + 'missed_cart_id' => [ + 'shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Required parameter "cart_id" is missing' + ], + 'missed_shipping_methods' => [ + 'cart_id: "cart_id_value"', + 'Required parameter "shipping_methods" is missing' + ], + 'shipping_methods_are_empty' => [ + 'cart_id: "cart_id_value" shipping_methods: []', + 'Required parameter "shipping_methods" is missing' + ], + 'missed_cart_address_id' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Required parameter "cart_address_id" is missing.' + ], + 'non_existent_cart_address_id' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: -1 + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Could not find a cart address with ID "-1"' + ], + 'missed_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + method_code: "flatrate" + }]', + 'Field ShippingMethodInput.carrier_code of required type String! was not provided.' + ], + 'empty_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "" + method_code: "flatrate" + }]', + 'Required parameter "carrier_code" is missing.' + ], + 'non_existent_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "wrong-carrier-code" + method_code: "flatrate" + }]', + 'Carrier with such method not found: wrong-carrier-code, flatrate' + ], + 'missed_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + }]', + 'Required parameter "method_code" is missing.' + ], + 'empty_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "" + }]', + 'Required parameter "method_code" is missing.' + ], + 'non_existent_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "wrong-carrier-code" + }]', + 'Carrier with such method not found: flatrate, wrong-carrier-code' + ], + 'non_existent_shopping_cart' => [ + 'cart_id: "non_existent_masked_id", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Could not find a cart with ID "non_existent_masked_id"' + ], + 'disabled_shipping_method' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "freeshipping" + method_code: "freeshipping" + }]', + 'Carrier with such method not found: freeshipping, freeshipping' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage You cannot specify multiple shipping methods. + */ + public function testSetMultipleShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$maskedQuoteId}", + shipping_methods: [ + { + cart_address_id: {$quoteAddressId} + carrier_code: "flatrate" + method_code: "flatrate" + } + { + cart_address_id: {$quoteAddressId} + carrier_code: "flatrate" + method_code: "flatrate" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + */ + public function testSetShippingMethodToGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + */ + public function testSetShippingMethodToAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/quote_with_address.php + */ + public function testSetShippingMethodIfCustomerIsNotOwnerOfAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $anotherQuoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('guest_quote_with_address'); + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $anotherQuoteAddressId + ); + + $this->expectExceptionMessage( + "Cart does not contain address with ID \"{$anotherQuoteAddressId}\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param int $shippingAddressId + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + int $shippingAddressId + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + cart_address_id: $shippingAddressId + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + } + } + } + } +} +QUERY; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping method can't be set for an empty cart. Add an item to cart and try again. + */ + public function testSetShippingMethodOnAnEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php new file mode 100644 index 0000000000000..35e2d62214fb2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php @@ -0,0 +1,334 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for updating shopping cart items + */ +class UpdateCartItemsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateCartItemQty() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + $qty = 2; + + $query = $this->getQuery($maskedQuoteId, $itemId, $qty); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $item = current($responseCart['items']); + + $this->assertEquals($itemId, $item['id']); + $this->assertEquals($qty, $item['qty']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testRemoveCartItemIfQuantityIsZero() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + $qty = 0; + + $query = $this->getQuery($maskedQuoteId, $itemId, $qty); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $this->assertCount(0, $responseCart['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testUpdateItemInNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateNonExistentItem() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Could not find cart item with id: {$notExistentItemId}."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemIfItemIsNotBelongToCart() + { + $firstQuote = $this->quoteFactory->create(); + $this->quoteResource->load($firstQuote, 'test_order_1', 'reserved_order_id'); + $firstQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId()); + + $secondQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $secondQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $secondQuote->setCustomerId(1); + $this->quoteResource->save($secondQuote); + $secondQuoteItemId = (int)$secondQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage("Could not find cart item with id: {$secondQuoteItemId}."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemInGuestCart() + { + $guestQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $guestQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId()); + $guestQuoteItemId = (int)$guestQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$guestQuoteMaskedId\"" + ); + + $query = $this->getQuery($guestQuoteMaskedId, $guestQuoteItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemInAnotherCustomerCart() + { + $anotherCustomerQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $anotherCustomerQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $anotherCustomerQuote->setCustomerId(2); + $this->quoteResource->save($anotherCustomerQuote); + + $anotherCustomerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$anotherCustomerQuote->getId()); + $anotherCustomerQuoteItemId = (int)$anotherCustomerQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$anotherCustomerQuoteMaskedId\"" + ); + + $query = $this->getQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing. + */ + public function testUpdateWithMissedCartItemId() + { + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_items: [ + { + cart_item_id: 1 + quantity: 2 + } + ] + }) { + cart { + items { + id + qty + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + {$input} + }) { + cart { + items { + id + qty + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_items' => [ + '', + 'Required parameter "cart_items" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_items: [{ quantity: 2 }]', + 'Required parameter "cart_item_id" for "cart_items" is missing.' + ], + 'missed_cart_item_qty' => [ + 'cart_items: [{ cart_item_id: 1 }]', + 'Required parameter "quantity" for "cart_items" is missing.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @param float $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId, float $qty): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + cart_items:[ + { + cart_item_id: {$itemId} + quantity: {$qty} + } + ] + }) { + cart { + items { + id + qty + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php new file mode 100644 index 0000000000000..9bb9bef9bdb09 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get masked quote id by reserved order id + */ +class GetMaskedQuoteIdByReservedOrderId +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + $this->quoteIdToMaskedId = $quoteIdToMaskedId; + } + + /** + * Get masked quote id by reserved order id + * + * @param string $reservedOrderId + * @return string + * @throws NoSuchEntityException + */ + public function execute(string $reservedOrderId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteItemIdByReservedQuoteIdAndSku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteItemIdByReservedQuoteIdAndSku.php new file mode 100644 index 0000000000000..6f027babc0e27 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteItemIdByReservedQuoteIdAndSku.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get quote item id by reserved order id and product sku + */ +class GetQuoteItemIdByReservedQuoteIdAndSku +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + * @param ProductRepositoryInterface $productRepository + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource, + ProductRepositoryInterface $productRepository + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + $this->productRepository = $productRepository; + } + + /** + * Get quote item id by reserved order id and product sku + * + * @param string $reservedOrderId + * @param string $sku + * @return int + * @throws NoSuchEntityException + */ + public function execute(string $reservedOrderId, string $sku): int + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $product = $this->productRepository->get($sku); + + return (int)$quote->getItemByProduct($product)->getId(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteShippingAddressIdByReservedQuoteId.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteShippingAddressIdByReservedQuoteId.php new file mode 100644 index 0000000000000..a56949b6f563a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteShippingAddressIdByReservedQuoteId.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get quote shipping address id by reserved order id + */ +class GetQuoteShippingAddressIdByReservedQuoteId +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + } + + /** + * Get quote shipping address id by reserved order id + * + * @param string $reservedOrderId + * @return int + */ + public function execute(string $reservedOrderId): int + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + + return (int)$quote->getShippingAddress()->getId(); + } +} 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 new file mode 100644 index 0000000000000..9e0693b160851 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product to cart testcases + */ +class AddSimpleProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddSimpleProductToCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][0]['qty']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddProductToNonExistentCart() + { + $sku = 'simple_product'; + $qty = 1; + $maskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "simple_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'simple_product'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddSimpleProductToCustomerCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, int $qty): string + { + return <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}" + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + qty + product { + sku + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php new file mode 100644 index 0000000000000..3f2d734635c3e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add virtual product to cart testcases + */ +class AddVirtualProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddVirtualProductToCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['addVirtualProductsToCart']); + self::assertEquals($qty, $response['addVirtualProductsToCart']['cart']['items'][0]['qty']); + self::assertEquals($sku, $response['addVirtualProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddVirtualToNonExistentCart() + { + $sku = 'virtual_product'; + $qty = 1; + $maskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "virtual_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'virtual_product'; + $qty = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddVirtualProductToCustomerCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param int $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, int $qty): string + { + return <<<QUERY +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{$maskedQuoteId}" + cartItems: [ + { + data: { + qty: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart { + items { + qty + product { + sku + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php new file mode 100644 index 0000000000000..affe36ea8617d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php @@ -0,0 +1,232 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test Apply Coupon to Cart functionality for guest + */ +class ApplyCouponToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponToCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + * @expectedExceptionMessage A coupon is already applied to the cart. Please remove it to apply another + */ + public function testApplyCouponTwice() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey("applyCouponToCart", $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products. + */ + public function testApplyCouponToCartWithoutItems() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + */ + public function testApplyCouponToCustomerCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyNonExistentCouponToCart() + { + $couponCode = 'non_existent_coupon_code'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponToNonExistentCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('Could not find a cart with ID "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_coupon_expired.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyExpiredCoupon() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/574'); + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * Products in cart don't fit to the coupon + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyCouponWhichIsNotApplicable() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @expectedException \Exception + */ + public function testApplyCouponWithMissedRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {{$input}}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'coupon_code: "test"', + 'Required parameter "cart_id" is missing' + ], + 'missed_coupon_code' => [ + 'cart_id: "test"', + 'Required parameter "coupon_code" is missing' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function getQuery(string $maskedQuoteId, string $couponCode): string + { + return <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php new file mode 100644 index 0000000000000..ee2d6a2b31de0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test getting cart totals for guest + */ +class CartTotalsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetCartTotalsWithTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('prices', $response['cart']); + $pricesResponse = $response['cart']['prices']; + self::assertEquals(21.5, $pricesResponse['grand_total']['value']); + self::assertEquals(21.5, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + + $appliedTaxesResponse = $pricesResponse['applied_taxes']; + + self::assertEquals('US-TEST-*-Rate-1', $appliedTaxesResponse[0]['label']); + self::assertEquals(1.5, $appliedTaxesResponse[0]['amount']['value']); + self::assertEquals('USD', $appliedTaxesResponse[0]['amount']['currency']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsWithNoTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * The totals calculation is based on quote address. + * But the totals should be calculated even if no address is set + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @group recent + */ + public function testGetCartTotalsWithNoAddressSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetSelectedShippingMethodFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * Generates GraphQl query for retrieving cart totals + * + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + prices { + grand_total { + value, + currency + } + subtotal_including_tax { + value + currency + } + subtotal_excluding_tax { + value + currency + } + subtotal_with_discount_excluding_tax { + value + currency + } + applied_taxes { + label + amount { + value + currency + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php new file mode 100644 index 0000000000000..f5114b9253a40 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php @@ -0,0 +1,441 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Framework\Registry; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * End to checkout tests for guest + */ +class CheckoutEndToEndTest extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + protected function setUp() + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->registry = $objectManager->get(Registry::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php + */ + public function testCheckoutWorkflow() + { + $qty = 2; + + $sku = $this->findProduct(); + $cartId = $this->createEmptyCart(); + $this->setGuestEmailOnCart($cartId); + $this->addProductToCart($cartId, $qty, $sku); + + $this->setBillingAddress($cartId); + $shippingAddress = $this->setShippingAddress($cartId); + + $shippingMethod = current($shippingAddress['available_shipping_methods']); + $paymentMethod = $this->setShippingMethod($cartId, $shippingAddress['address_id'], $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $this->placeOrder($cartId); + } + + /** + * @return string + */ + private function findProduct(): string + { + $query = <<<QUERY +{ + products ( + filter: { + sku: { + like:"simple%" + } + } + pageSize: 1 + currentPage: 1 + ) { + items { + sku + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + + $product = current($response['products']['items']); + self::assertArrayHasKey('sku', $product); + self::assertNotEmpty($product['sku']); + + return $product['sku']; + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + return $response['createEmptyCart']; + } + + /** + * @param string $cartId + * @return void + */ + private function setGuestEmailOnCart(string $cartId): void + { + $query = <<<QUERY +mutation { + setGuestEmailOnCart( + input: { + cart_id: "{$cartId}" + email: "customer@example.com" + } + ) { + cart { + email + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cartItems: [ + { + data: { + qty: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart { + items { + qty + product { + sku + } + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @param array $auth + * @return array + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + postcode: "887766" + telephone: "88776655" + region: "TX" + country_code: "US" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + address_type + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "TX" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + address_id + available_shipping_methods { + carrier_code + method_code + amount + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('setShippingAddressesOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingAddressesOnCart']['cart']); + self::assertCount(1, $response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('address_id', $shippingAddress); + self::assertNotEmpty($shippingAddress['address_id']); + self::assertArrayHasKey('available_shipping_methods', $shippingAddress); + self::assertCount(1, $shippingAddress['available_shipping_methods']); + + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + self::assertArrayHasKey('carrier_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['carrier_code']); + + self::assertArrayHasKey('method_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['method_code']); + + self::assertArrayHasKey('amount', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['amount']); + + return $shippingAddress; + } + + /** + * @param string $cartId + * @param int $addressId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, int $addressId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + cart_address_id: {$addressId} + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('available_payment_methods', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + self::assertArrayHasKey('code', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['code']); + self::assertArrayHasKey('title', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['title']); + + return $availablePaymentMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @return void + */ + private function placeOrder(string $cartId): void + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_id + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_id']); + } + + public function tearDown() + { + $this->deleteQuote(); + $this->deleteOrder(); + parent::tearDown(); + } + + /** + * @return void + */ + private function deleteQuote(): void + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + } + + /** + * @return void + */ + private function deleteOrder() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php new file mode 100644 index 0000000000000..6ed91d21f0ae2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Quote\Api\GuestCartRepositoryInterface; + +/** + * Test for empty cart creation mutation + */ +class CreateEmptyCartTest extends GraphQlAbstract +{ + /** + * @var GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + public function testCreateEmptyCart() + { + $query = $this->getQuery(); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + self::assertEquals('default', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store.php + */ + public function testCreateEmptyCartWithNotDefaultStore() + { + $query = $this->getQuery(); + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlMutation($query, [], '', $headerMap); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + $this->maskedQuoteId = $response['createEmptyCart']; + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + self::assertSame('fixture_second_store', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCartWithPredefinedCartId() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertEquals($predefinedCartId, $response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart with ID "572cda51902b5b517c0e1a2b2fd004b4" already exists. + */ + public function testCreateEmptyCartIfPredefinedCartIdAlreadyExists() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart ID length should to be 32 symbols. + */ + public function testCreateEmptyCartWithWrongPredefinedCartId() + { + $predefinedCartId = '572'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @return string + */ + private function getQuery(): string + { + return <<<QUERY +mutation { + createEmptyCart +} +QUERY; + } + + public function tearDown() + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php new file mode 100644 index 0000000000000..af1f72fe71620 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetAvailablePaymentMethodsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); + self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethodsFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php + */ + public function testGetAvailablePaymentMethodsIfPaymentsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEmpty($response['cart']['available_payment_methods']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailablePaymentMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + available_payment_methods { + code + title + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php new file mode 100644 index 0000000000000..a8113657eff6e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get available shipping methods + */ +class GetAvailableShippingMethodsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * Test case: get available shipping methods from current customer quote + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId)); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + self::assertArrayHasKey('available_shipping_methods', $response['cart']['shipping_addresses'][0]); + self::assertCount(1, $response['cart']['shipping_addresses'][0]['available_shipping_methods']); + + $expectedAddressData = [ + 'amount' => 10, + 'base_amount' => 10, + 'carrier_code' => 'flatrate', + 'carrier_title' => 'Flat Rate', + 'error_message' => '', + 'method_code' => 'flatrate', + 'method_title' => 'Fixed', + 'price_incl_tax' => 10, + 'price_excl_tax' => 10, + ]; + self::assertEquals( + $expectedAddressData, + $response['cart']['shipping_addresses'][0]['available_shipping_methods'][0] + ); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethodsFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($this->getQuery($maskedQuoteId)); + } + + /** + * Test case: get available shipping methods when all shipping methods are disabled + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php + */ + public function testGetAvailableShippingMethodsIfShippingMethodsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId)); + + self::assertEmpty($response['cart']['shipping_addresses'][0]['available_shipping_methods']); + } + + /** + * Test case: get available shipping methods from non-existent cart + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailableShippingMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +query { + cart (cart_id: "{$maskedQuoteId}") { + shipping_addresses { + available_shipping_methods { + amount + base_amount + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax + price_incl_tax + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartEmailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartEmailTest.php new file mode 100644 index 0000000000000..8c6ecd075049f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartEmailTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting email from cart + */ +class GetCartEmailTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + */ + public function testGetCartEmail() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('email', $response['cart']); + $this->assertEquals('guest@example.com', $response['cart']['email']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetCartEmailFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetCartEmailFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id:"$maskedQuoteId") { + email + } +} +QUERY; + } +} 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 new file mode 100644 index 0000000000000..832e15058a4ee --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertCount(2, $response['cart']['items']); + + self::assertNotEmpty($response['cart']['items'][0]['id']); + self::assertEquals(2, $response['cart']['items'][0]['qty']); + self::assertEquals('simple_product', $response['cart']['items'][0]['product']['sku']); + + self::assertNotEmpty($response['cart']['items'][1]['id']); + self::assertEquals(2, $response['cart']['items'][1]['qty']); + self::assertEquals('virtual-product', $response['cart']['items'][1]['product']['sku']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current user does not have an active cart. + */ + public function testGetInactiveCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_guest_not_default_store.php + */ + public function testGetCartWithNotDefaultStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store_guest'); + $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/active_quote.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Wrong store code specified for cart + */ + public function testGetCartWithWrongStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_guest_not_default_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Store code not_existing_store does not exist + */ + public function testGetCartWithNotExistingStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store_guest'); + + $headerMap['Store'] = 'not_existing_store'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "{$maskedQuoteId}") { + items { + id + qty + product { + sku + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php new file mode 100644 index 0000000000000..bfdecca782319 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get selected shipping method + */ +class GetSelectedShippingMethodTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethodFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); + self::assertNull($shippingAddress['selected_shipping_method']['method_code']); + self::assertNull($shippingAddress['selected_shipping_method']['label']); + self::assertNull($shippingAddress['selected_shipping_method']['amount']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetSelectedShippingMethodOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + amount + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php new file mode 100644 index 0000000000000..d592443aed499 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get specified billing address + */ +class GetSpecifiedBillingAddressTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => 'CompanyName', + 'street' => [ + 'Green str, 67' + ], + 'city' => 'CityM', + 'region' => [ + 'code' => 'AL', + 'label' => 'Alabama', + ], + 'postcode' => '75477', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + 'telephone' => '3468676', + 'address_type' => 'BILLING', + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGeSpecifiedBillingAddressIfBillingAddressIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => null, + 'lastname' => null, + 'company' => null, + 'street' => [ + '' + ], + 'city' => null, + 'region' => [ + 'code' => null, + 'label' => null, + ], + 'postcode' => null, + 'country' => [ + 'code' => null, + 'label' => null, + ], + 'telephone' => null, + 'address_type' => 'BILLING', + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetBillingAddressOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetBillingAddressFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + billing_address { + firstname + lastname + company + street + city + region + { + code + label + } + postcode + country + { + code + label + } + telephone + address_type + } + } +} +QUERY; + } +} 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 new file mode 100644 index 0000000000000..30ad69eada29d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -0,0 +1,273 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Framework\Registry; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for placing an order for guest + */ +class PlaceOrderTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @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_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 testPlaceOrder() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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 testPlaceOrderWithNoEmail() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage("Guest email for cart is missing. Please enter"); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + */ + public function testPlaceOrderWithNoItemsInCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: A server error stopped your order from being placed. ' . + 'Please try to place your order again' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @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 + */ + public function testPlaceOrderWithNoShippingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: Some addresses can\'t be used due to the configurations for specific countries' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @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_new_shipping_address.php + */ + public function testPlaceOrderWithNoShippingMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: The shipping method is missing. Select the shipping method and try again' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @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_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoBillingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp( + '/Unable to place order: Please check the billing address information*/' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @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_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoPaymentMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Enter a valid payment method and try again'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @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_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/Catalog/_files/set_simple_product_out_of_stock.php + */ + public function testPlaceOrderWithOutOfStockProduct() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Some of the products are out of stock'); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.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 testPlaceOrderOfCustomerCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp('/The current user cannot perform operations on cart*/'); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { + order { + order_id + } + } +} +QUERY; + } + + /** + * @inheritdoc + */ + public function tearDown() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php new file mode 100644 index 0000000000000..5adb6ce65db6f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Check removing of the coupon from guest cart + */ +class RemoveCouponFromCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveCouponFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products + */ + public function testRemoveCouponFromEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveCouponFromCartIfCouponWasNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + removeCouponFromCart(input: {cart_id: "{$maskedQuoteId}"}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php new file mode 100644 index 0000000000000..27f3f6367f662 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php @@ -0,0 +1,189 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for removeItemFromCartTest mutation + */ +class RemoveItemFromCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteItemIdByReservedQuoteIdAndSku + */ + private $getQuoteItemIdByReservedQuoteIdAndSku; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteItemIdByReservedQuoteIdAndSku = $objectManager->get( + GetQuoteItemIdByReservedQuoteIdAndSku::class + ); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $itemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $query = $this->getQuery($maskedQuoteId, $itemId); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('removeItemFromCart', $response); + $this->assertArrayHasKey('cart', $response['removeItemFromCart']); + $this->assertCount(0, $response['removeItemFromCart']['cart']['items']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveItemFromNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveNonExistentItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Cart doesn't contain the {$notExistentItemId} item."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId); + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + removeItemFromCart( + input: { + {$input} + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'cart_item_id: 1', + 'Required parameter "cart_id" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_id: "test"', + 'Required parameter "cart_item_id" is missing.' + ], + ]; + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testRemoveItemIfItemIsNotBelongToCart() + { + $firstQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $secondQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute( + 'test_order_with_virtual_product_without_address', + 'virtual-product' + ); + + $this->expectExceptionMessage("Cart doesn't contain the {$secondQuoteItemId} item."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromCustomerCart() + { + $customerQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $customerQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $this->expectExceptionMessage("The current user cannot perform operations on cart \"$customerQuoteMaskedId\""); + + $query = $this->getQuery($customerQuoteMaskedId, $customerQuoteItemId); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId): string + { + return <<<QUERY +mutation { + removeItemFromCart( + input: { + cart_id: "{$maskedQuoteId}" + cart_item_id: {$itemId} + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php new file mode 100644 index 0000000000000..d2d53220f0042 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -0,0 +1,399 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set billing address on cart mutation + */ +class SetBillingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithUseForShippingParameter() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + use_for_shipping: true + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'SHIPPING'); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testSetBillingAddressToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testSetBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetBillingAddressOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderSetWithoutRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetBillingAddressWithoutRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + {$input} + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderSetWithoutRequiredParameters(): array + { + return [ + 'missed_billing_address' => [ + 'cart_id: "cart_id_value"', + 'Field SetBillingAddressOnCartInput.billing_address of required type BillingAddressInput!' + . ' was not provided.', + ], + 'missed_cart_id' => [ + 'billing_address: {}', + 'Required parameter "cart_id" is missing' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + } + } + } +} +QUERY; + + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $addressResponse + * @param string $addressType + */ + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BILLING'): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => $addressType] + ]; + + $this->assertResponseFields($addressResponse, $assertionMap); + } +} 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 new file mode 100644 index 0000000000000..b877dccdeba37 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setGuestEmailOnCart mutation + */ +class SetGuestEmailOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testSetGuestEmailOnCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('setGuestEmailOnCart', $response); + $this->assertArrayHasKey('cart', $response['setGuestEmailOnCart']); + $this->assertEquals($email, $response['setGuestEmailOnCart']['cart']['email']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testSetGuestEmailOnCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @dataProvider incorrectEmailDataProvider + * @param string $email + * @param string $exceptionMessage + */ + public function testSetGuestEmailOnCartWithIncorrectEmail( + string $email, + string $exceptionMessage + ) { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $email); + $this->expectExceptionMessage($exceptionMessage); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function incorrectEmailDataProvider(): array + { + return [ + 'wrong_email' => ['some', 'Invalid email format'], + 'no_email' => ['', 'Required parameter "email" is missing'], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetGuestEmailOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing + */ + public function testSetGuestEmailWithEmptyCartId() + { + $maskedQuoteId = ''; + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query); + } + + /** + * Returns GraphQl mutation query for setting email address for a guest + * + * @param string $maskedQuoteId + * @param string $email + * @return string + */ + private function getQuery(string $maskedQuoteId, string $email): string + { + return <<<QUERY +mutation { + setGuestEmailOnCart(input: { + cart_id: "$maskedQuoteId" + email: "$email" + }) { + cart { + email + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..2c1333aa77326 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteShippingAddressIdByReservedQuoteId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting offline shipping methods on cart + */ +class SetOfflineShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteShippingAddressIdByReservedQuoteId + */ + private $getQuoteShippingAddressIdByReservedQuoteId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteShippingAddressIdByReservedQuoteId = $objectManager->get( + GetQuoteShippingAddressIdByReservedQuoteId::class + ); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/OfflineShipping/_files/tablerates_weight.php + * + * @param string $carrierCode + * @param string $methodCode + * @param float $amount + * @param string $label + * @dataProvider offlineShippingMethodDataProvider + */ + public function testSetOfflineShippingMethod(string $carrierCode, string $methodCode, float $amount, string $label) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + self::assertEquals($amount, $shippingAddress['selected_shipping_method']['amount']); + + self::assertArrayHasKey('label', $shippingAddress['selected_shipping_method']); + self::assertEquals($label, $shippingAddress['selected_shipping_method']['label']); + } + + /** + * @return array + */ + public function offlineShippingMethodDataProvider(): array + { + return [ + 'flatrate_flatrate' => ['flatrate', 'flatrate', 10, 'Flat Rate - Fixed'], + 'tablerate_bestway' => ['tablerate', 'bestway', 10, 'Best Way - Table Rate'], + 'freeshipping_freeshipping' => ['freeshipping', 'freeshipping', 0, 'Free Shipping - Free'], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param int $shippingAddressId + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + int $shippingAddressId + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + cart_address_id: $shippingAddressId + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + amount + label + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php new file mode 100644 index 0000000000000..4ea7eac290f80 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php @@ -0,0 +1,264 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\OfflinePayments\Model\Cashondelivery; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting payment methods on cart by guest + */ +class SetPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetPaymentOnCartWithSimpleProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentOnCartWithSimpleProductAndWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testSetPaymentOnCartWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetNonExistentPaymentMethod() + { + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetPaymentOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetPaymentMethodToCustomerCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetPaymentMethodWithoutRequiredParameters + * @throws Exception + */ + public function testSetPaymentMethodWithoutRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + {$input} + } + ) { + cart { + items { + qty + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', + 'Required parameter "cart_id" is missing.' + ], + 'missed_payment_method' => [ + 'cart_id: "test"', + 'Required parameter "code" for "payment_method" is missing.' + ], + 'missed_payment_method_code' => [ + 'cart_id: "test", payment_method: {code: ""}', + 'Required parameter "code" for "payment_method" is missing.' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testReSetPayment() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $methodCode = Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE; + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetDisabledPaymentOnCart() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $methodCode + ) : string { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php new file mode 100644 index 0000000000000..888b0e87734b6 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -0,0 +1,391 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set shipping addresses on cart mutation + */ +class SetShippingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + address_type + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. + */ + public function testSetNewShippingAddressOnCartWithVirtualProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testSetShippingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + */ + public function testSetShippingAddressToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + postcode + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetNewShippingAddressWithMissedRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$maskedQuoteId}" + shipping_addresses: [ + { + {$input} + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + } + } + } +} +QUERY; + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'shipping_addresses' => [ + '', + 'The shipping address must contain either "customer_address_id" or "address".', + ], + 'missed_city' => [ + 'address: { save_in_address_book: false }', + 'Field CartAddressInput.city of required type String! was not provided' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage You cannot specify multiple shipping addresses. + */ + public function testSetMultipleNewShippingAddresses() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + }, + { + address: { + firstname: "test firstname 2" + lastname: "test lastname 2" + company: "test company 2" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $shippingAddressResponse + */ + private function assertNewShippingAddressFields(array $shippingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => 'address_type', 'expected_value' => 'SHIPPING'] + ]; + + $this->assertResponseFields($shippingAddressResponse, $assertionMap); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..59f53d2ad6856 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -0,0 +1,445 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteShippingAddressIdByReservedQuoteId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting shipping methods on cart for guest + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteShippingAddressIdByReservedQuoteId + */ + private $getQuoteShippingAddressIdByReservedQuoteId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteShippingAddressIdByReservedQuoteId = $objectManager->get( + GetQuoteShippingAddressIdByReservedQuoteId::class + ); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetShippingMethodOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * Shipping address for quote will be created automatically BUT with NULL values (considered that address + * is not set) + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetShippingMethodOnCartWithSimpleProductAndWithoutAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testReSetShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'freeshipping'; + $methodCode = 'freeshipping'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetShippingMethodWithWrongParameters + * @throws Exception + */ + public function testSetShippingMethodWithWrongParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + $input = str_replace(['cart_id_value', 'cart_address_id_value'], [$maskedQuoteId, $quoteAddressId], $input); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + {$input} + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function dataProviderSetShippingMethodWithWrongParameters(): array + { + return [ + 'missed_cart_id' => [ + 'shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Required parameter "cart_id" is missing' + ], + 'missed_shipping_methods' => [ + 'cart_id: "cart_id_value"', + 'Required parameter "shipping_methods" is missing' + ], + 'shipping_methods_are_empty' => [ + 'cart_id: "cart_id_value" shipping_methods: []', + 'Required parameter "shipping_methods" is missing' + ], + 'missed_cart_address_id' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Required parameter "cart_address_id" is missing.' + ], + 'non_existent_cart_address_id' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: -1 + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Could not find a cart address with ID "-1"' + ], + 'missed_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + method_code: "flatrate" + }]', + 'Field ShippingMethodInput.carrier_code of required type String! was not provided.' + ], + 'empty_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "" + method_code: "flatrate" + }]', + 'Required parameter "carrier_code" is missing.' + ], + 'non_existent_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "wrong-carrier-code" + method_code: "flatrate" + }]', + 'Carrier with such method not found: wrong-carrier-code, flatrate' + ], + 'missed_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + }]', + 'Required parameter "method_code" is missing.' + ], + 'empty_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "" + }]', + 'Required parameter "method_code" is missing.' + ], + 'non_existent_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "wrong-carrier-code" + }]', + 'Carrier with such method not found: flatrate, wrong-carrier-code' + ], + 'non_existent_shopping_cart' => [ + 'cart_id: "non_existent_masked_id", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Could not find a cart with ID "non_existent_masked_id"' + ], + 'disabled_shipping_method' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + cart_address_id: cart_address_id_value + carrier_code: "freeshipping" + method_code: "freeshipping" + }]', + 'Carrier with such method not found: freeshipping, freeshipping' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage You cannot specify multiple shipping methods. + */ + public function testSetMultipleShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$maskedQuoteId}", + shipping_methods: [ + { + cart_address_id: {$quoteAddressId} + carrier_code: "flatrate" + method_code: "flatrate" + } + { + cart_address_id: {$quoteAddressId} + carrier_code: "flatrate" + method_code: "flatrate" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + */ + public function testSetShippingMethodToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/quote_with_address.php + */ + public function testSetShippingMethodIfGuestIsNotOwnerOfAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $anotherQuoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('guest_quote_with_address'); + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $anotherQuoteAddressId + ); + + $this->expectExceptionMessage( + "Cart does not contain address with ID \"{$anotherQuoteAddressId}\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/quote_with_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping method can't be set for an empty cart. Add an item to cart and try again. + */ + public function testSetShippingMethodOnAnEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $quoteAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode, + $quoteAddressId + ); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @param int $shippingAddressId + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode, + int $shippingAddressId + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + cart_address_id: $shippingAddressId + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php new file mode 100644 index 0000000000000..1b8cf2e1c57f7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -0,0 +1,273 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for updating/removing shopping cart items + */ +class UpdateCartItemsTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testUpdateCartItemQty() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + $qty = 2; + + $query = $this->getQuery($maskedQuoteId, $itemId, $qty); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $item = current($responseCart['items']); + + $this->assertEquals($itemId, $item['id']); + $this->assertEquals($qty, $item['qty']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testRemoveCartItemIfQuantityIsZero() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + $qty = 0; + + $query = $this->getQuery($maskedQuoteId, $itemId, $qty); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $this->assertCount(0, $responseCart['items']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testUpdateItemInNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1, 2); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testUpdateNonExistentItem() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Could not find cart item with id: {$notExistentItemId}."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId, 2); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemIfItemIsNotBelongToCart() + { + $firstQuote = $this->quoteFactory->create(); + $this->quoteResource->load($firstQuote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $firstQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId()); + + $secondQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $secondQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $secondQuoteItemId = (int)$secondQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage("Could not find cart item with id: {$secondQuoteItemId}."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId, 2); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateItemFromCustomerCart() + { + $customerQuote = $this->quoteFactory->create(); + $this->quoteResource->load($customerQuote, 'test_order_1', 'reserved_order_id'); + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId()); + $customerQuoteItemId = (int)$customerQuote->getItemByProduct($this->productRepository->get('simple'))->getId(); + + $this->expectExceptionMessage("The current user cannot perform operations on cart \"$customerQuoteMaskedId\""); + + $query = $this->getQuery($customerQuoteMaskedId, $customerQuoteItemId, 2); + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing. + */ + public function testUpdateWithMissedCartItemId() + { + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_items: [ + { + cart_item_id: 1 + quantity: 2 + } + ] + }) { + cart { + items { + id + qty + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + {$input} + }) { + cart { + items { + id + qty + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_items' => [ + '', + 'Required parameter "cart_items" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_items: [{ quantity: 2 }]', + 'Required parameter "cart_item_id" for "cart_items" is missing.' + ], + 'missed_cart_item_qty' => [ + 'cart_items: [{ cart_item_id: 1 }]', + 'Required parameter "quantity" for "cart_items" is missing.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @param float $qty + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId, float $qty): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + cart_items: [ + { + cart_item_id: {$itemId} + quantity: {$qty} + } + ] + }) { + cart { + items { + id + qty + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php deleted file mode 100644 index a023d37895c23..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php +++ /dev/null @@ -1,469 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Quote; - -use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\TestFramework\ObjectManager; - -/** - * Test for set shipping addresses on cart mutation - */ -class SetShippingAddressOnCartTest extends GraphQlAbstract -{ - /** - * @var QuoteResource - */ - private $quoteResource; - - /** - * @var Quote - */ - private $quote; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; - - protected function setUp() - { - $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->create(QuoteResource::class); - $this->quote = $objectManager->create(Quote::class); - $this->quoteIdToMaskedId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - */ - public function testSetNewGuestShippingAddressOnCart() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - { - address: { - firstname: "test firstname" - lastname: "test lastname" - company: "test company" - street: ["test street 1", "test street 2"] - city: "test city" - region: "test region" - postcode: "887766" - country_code: "US" - telephone: "88776655" - save_in_address_book: false - } - } - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - $response = $this->graphQlQuery($query); - - self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); - $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); - $this->assertNewShippingAddressFields($shippingAddressResponse); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - */ - public function testSetSavedShippingAddressOnCartByGuest() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - { - customer_address_id: 1 - } - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - self::expectExceptionMessage('The current customer isn\'t authorized.'); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - */ - public function testSetMultipleShippingAddressesOnCartByGuest() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - { - customer_address_id: 1 - }, - { - customer_address_id: 1 - } - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - self::expectExceptionMessage('You cannot specify multiple shipping addresses.'); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - */ - public function testSetSavedAndNewShippingAddressOnCartAtTheSameTime() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - { - customer_address_id: 1, - address: { - firstname: "test firstname" - lastname: "test lastname" - company: "test company" - street: ["test street 1", "test street 2"] - city: "test city" - region: "test region" - postcode: "887766" - country_code: "US" - telephone: "88776655" - save_in_address_book: false - } - } - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - self::expectExceptionMessage( - 'The shipping address cannot contain "customer_address_id" and "address" at the same time.' - ); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - */ - public function testSetShippingAddressOnCartWithNoAddresses() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - {} - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - self::expectExceptionMessage( - 'The shipping address must contain either "customer_address_id" or "address".' - ); - $this->graphQlQuery($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/Customer/_files/customer.php - */ - public function testSetNewRegisteredCustomerShippingAddressOnCart() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $this->quote->setCustomerId(1); - $this->quoteResource->save($this->quote); - - $headerMap = $this->getHeaderMap(); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - { - address: { - firstname: "test firstname" - lastname: "test lastname" - company: "test company" - street: ["test street 1", "test street 2"] - city: "test city" - region: "test region" - postcode: "887766" - country_code: "US" - telephone: "88776655" - save_in_address_book: false - } - } - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - $response = $this->graphQlQuery($query, [], '', $headerMap); - - self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); - $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); - $this->assertNewShippingAddressFields($shippingAddressResponse); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php - * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php - */ - public function testSetSavedRegisteredCustomerShippingAddressOnCart() - { - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $this->quoteResource->load( - $this->quote, - 'test_order_with_simple_product_without_address', - 'reserved_order_id' - ); - $this->quote->setCustomerId(1); - $this->quoteResource->save($this->quote); - - $headerMap = $this->getHeaderMap(); - - $query = <<<QUERY -mutation { - setShippingAddressesOnCart( - input: { - cart_id: "$maskedQuoteId" - shipping_addresses: [ - { - customer_address_id: 1 - } - ] - } - ) { - cart { - addresses { - firstname - lastname - company - street - city - postcode - telephone - } - } - } -} -QUERY; - $response = $this->graphQlQuery($query, [], '', $headerMap); - - self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); - $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); - $this->assertSavedShippingAddressFields($shippingAddressResponse); - } - - /** - * Verify the all the whitelisted fields for a New Address Object - * - * @param array $shippingAddressResponse - */ - private function assertNewShippingAddressFields(array $shippingAddressResponse): void - { - $assertionMap = [ - ['response_field' => 'firstname', 'expected_value' => 'test firstname'], - ['response_field' => 'lastname', 'expected_value' => 'test lastname'], - ['response_field' => 'company', 'expected_value' => 'test company'], - ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], - ['response_field' => 'city', 'expected_value' => 'test city'], - ['response_field' => 'postcode', 'expected_value' => '887766'], - ['response_field' => 'telephone', 'expected_value' => '88776655'] - ]; - - $this->assertResponseFields($shippingAddressResponse, $assertionMap); - } - - /** - * Verify the all the whitelisted fields for a Address Object - * - * @param array $shippingAddressResponse - */ - private function assertSavedShippingAddressFields(array $shippingAddressResponse): void - { - $assertionMap = [ - ['response_field' => 'firstname', 'expected_value' => 'John'], - ['response_field' => 'lastname', 'expected_value' => 'Smith'], - ['response_field' => 'company', 'expected_value' => 'CompanyName'], - ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']], - ['response_field' => 'city', 'expected_value' => 'CityM'], - ['response_field' => 'postcode', 'expected_value' => '75477'], - ['response_field' => 'telephone', 'expected_value' => '3468676'] - ]; - - $this->assertResponseFields($shippingAddressResponse, $assertionMap); - } - - /** - * @param string $username - * @return array - */ - private function getHeaderMap(string $username = 'customer@example.com'): array - { - $password = 'password'; - /** @var CustomerTokenServiceInterface $customerTokenService */ - $customerTokenService = ObjectManager::getInstance() - ->get(CustomerTokenServiceInterface::class); - $customerToken = $customerTokenService->createCustomerAccessToken($username, $password); - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - return $headerMap; - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php deleted file mode 100644 index 7e77284c6b220..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php +++ /dev/null @@ -1,252 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Quote; - -use Magento\Integration\Api\CustomerTokenServiceInterface; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\TestCase\GraphQlAbstract; - -/** - * Test for setting shipping methods on cart - */ -class SetShippingMethodOnCartTest extends GraphQlAbstract -{ - /** - * @var CustomerTokenServiceInterface - */ - private $customerTokenService; - - /** - * @var QuoteResource - */ - private $quoteResource; - - /** - * @var Quote - */ - private $quote; - - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedId; - - /** - * @inheritdoc - */ - protected function setUp() - { - $objectManager = Bootstrap::getObjectManager(); - $this->quoteResource = $objectManager->create(QuoteResource::class); - $this->quote = $objectManager->create(Quote::class); - $this->quoteIdToMaskedId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class); - $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - */ - public function testSetShippingMethodOnCart() - { - $shippingCarrierCode = 'flatrate'; - $shippingMethodCode = 'flatrate'; - $this->quoteResource->load( - $this->quote, - 'test_order_1', - 'reserved_order_id' - ); - $shippingAddress = $this->quote->getShippingAddress(); - $shippingAddressId = $shippingAddress->getId(); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = $this->prepareMutationQuery( - $maskedQuoteId, - $shippingMethodCode, - $shippingCarrierCode, - $shippingAddressId - ); - - $response = $this->sendRequestWithToken($query); - - self::assertArrayHasKey('setShippingMethodsOnCart', $response); - self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); - self::assertEquals($maskedQuoteId, $response['setShippingMethodsOnCart']['cart']['cart_id']); - $addressesInformation = $response['setShippingMethodsOnCart']['cart']['addresses']; - self::assertCount(2, $addressesInformation); - self::assertEquals( - $addressesInformation[0]['selected_shipping_method']['code'], - $shippingCarrierCode . '_' . $shippingMethodCode - ); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - */ - public function testSetShippingMethodWithWrongCartId() - { - $shippingCarrierCode = 'flatrate'; - $shippingMethodCode = 'flatrate'; - $shippingAddressId = '1'; - $maskedQuoteId = 'invalid'; - - $query = $this->prepareMutationQuery( - $maskedQuoteId, - $shippingMethodCode, - $shippingCarrierCode, - $shippingAddressId - ); - - self::expectExceptionMessage("Could not find a cart with ID \"$maskedQuoteId\""); - $this->sendRequestWithToken($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - */ - public function testSetNonExistingShippingMethod() - { - $shippingCarrierCode = 'non'; - $shippingMethodCode = 'existing'; - $this->quoteResource->load( - $this->quote, - 'test_order_1', - 'reserved_order_id' - ); - $shippingAddress = $this->quote->getShippingAddress(); - $shippingAddressId = $shippingAddress->getId(); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = $this->prepareMutationQuery( - $maskedQuoteId, - $shippingMethodCode, - $shippingCarrierCode, - $shippingAddressId - ); - - self::expectExceptionMessage("Carrier with such method not found: $shippingCarrierCode, $shippingMethodCode"); - $this->sendRequestWithToken($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - */ - public function testSetShippingMethodWithNonExistingAddress() - { - $shippingCarrierCode = 'flatrate'; - $shippingMethodCode = 'flatrate'; - $this->quoteResource->load( - $this->quote, - 'test_order_1', - 'reserved_order_id' - ); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - $shippingAddressId = '-20'; - - $query = $this->prepareMutationQuery( - $maskedQuoteId, - $shippingMethodCode, - $shippingCarrierCode, - $shippingAddressId - ); - - self::expectExceptionMessage('The shipping address is missing. Set the address and try again.'); - $this->sendRequestWithToken($query); - } - - /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php - */ - public function testSetShippingMethodByGuestToCustomerCart() - { - $shippingCarrierCode = 'flatrate'; - $shippingMethodCode = 'flatrate'; - $this->quoteResource->load( - $this->quote, - 'test_order_1', - 'reserved_order_id' - ); - $shippingAddress = $this->quote->getShippingAddress(); - $shippingAddressId = $shippingAddress->getId(); - $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); - - $query = $this->prepareMutationQuery( - $maskedQuoteId, - $shippingMethodCode, - $shippingCarrierCode, - $shippingAddressId - ); - - self::expectExceptionMessage( - "The current user cannot perform operations on cart \"$maskedQuoteId\"" - ); - - $this->graphQlQuery($query); - } - - /** - * Generates query for setting the specified shipping method on cart - * - * @param string $maskedQuoteId - * @param string $shippingMethodCode - * @param string $shippingCarrierCode - * @param string $shippingAddressId - * @return string - */ - private function prepareMutationQuery( - string $maskedQuoteId, - string $shippingMethodCode, - string $shippingCarrierCode, - string $shippingAddressId - ) : string { - return <<<QUERY -mutation { - setShippingMethodsOnCart(input: - { - cart_id: "$maskedQuoteId", - shipping_methods: [ - { - shipping_method_code: "$shippingMethodCode" - shipping_carrier_code: "$shippingCarrierCode" - cart_address_id: $shippingAddressId - } - ]}) { - - cart { - cart_id, - addresses { - selected_shipping_method { - code - label - } - } - } - } -} - -QUERY; - } - - /** - * Sends a GraphQL request with using a bearer token - * - * @param string $query - * @return array - * @throws \Magento\Framework\Exception\AuthenticationException - */ - private function sendRequestWithToken(string $query): array - { - - $customerToken = $this->customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - - return $this->graphQlQuery($query, [], '', $headerMap); - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php new file mode 100644 index 0000000000000..ae6faae7650b9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php @@ -0,0 +1,361 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\SendFriend; + +use Magento\SendFriend\Model\SendFriend; +use Magento\SendFriend\Model\SendFriendFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for send email to friend + */ +class SendFriendTest extends GraphQlAbstract +{ + + /** + * @var SendFriendFactory + */ + private $sendFriendFactory; + + protected function setUp() + { + $this->sendFriendFactory = Bootstrap::getObjectManager()->get(SendFriendFactory::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSendFriend() + { + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + self::assertEquals('Name', $response['sendEmailToFriend']['sender']['name']); + self::assertEquals('e@mail.com', $response['sendEmailToFriend']['sender']['email']); + self::assertEquals('Lorem Ipsum', $response['sendEmailToFriend']['sender']['message']); + self::assertEquals('Recipient Name 1', $response['sendEmailToFriend']['recipients'][0]['name']); + self::assertEquals('recipient1@mail.com', $response['sendEmailToFriend']['recipients'][0]['email']); + self::assertEquals('Recipient Name 2', $response['sendEmailToFriend']['recipients'][1]['name']); + self::assertEquals('recipient2@mail.com', $response['sendEmailToFriend']['recipients'][1]['email']); + } + + public function testSendWithoutExistProduct() + { + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 2018 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testMaxSendEmailToFriend() + { + /** @var SendFriend $sendFriend */ + $sendFriend = $this->sendFriendFactory->create(); + + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + } + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage("No more than {$sendFriend->getMaxRecipients()} emails can be sent at a time."); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @dataProvider sendFriendsErrorsDataProvider + * @param string $input + * @param string $errorMessage + */ + public function testErrors(string $input, string $errorMessage) + { + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + $input + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage($errorMessage); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * TODO: use magentoApiConfigFixture (to be merged https://github.com/magento/graphql-ce/pull/351) + * @magentoApiDataFixture Magento/SendFriend/Fixtures/sendfriend_configuration.php + */ + public function testLimitMessagesPerHour() + { + + /** @var SendFriend $sendFriend */ + $sendFriend = $this->sendFriendFactory->create(); + + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + "You can't send messages more than {$sendFriend->getMaxSendsToFriend()} times an hour." + ); + + $maxSendToFriends = $sendFriend->getMaxSendsToFriend(); + for ($i = 0; $i <= $maxSendToFriends + 1; $i++) { + $this->graphQlMutation($query); + } + } + + /** + * @return array + */ + public function sendFriendsErrorsDataProvider() + { + return [ + [ + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "" + email:"recipient1@mail.com" + }, + { + name: "" + email:"recipient2@mail.com" + } + ]', 'Please provide Name for all of recipients.' + ], + [ + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"" + }, + { + name: "Recipient Name 2" + email:"" + } + ]', 'Please provide Email for all of recipients.' + ], + [ + 'product_id: 1 + sender: { + name: "" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', 'Please provide Name of sender.' + ], + [ + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', 'Please provide Message.' + ] + ]; + } +} 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 b6e1a61f0357c..c85f63c083700 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php @@ -28,10 +28,31 @@ public function testMutation() } MUTATION; - $response = $this->graphQlQuery($query); + $response = $this->graphQlMutation($query); $this->assertArrayHasKey('testItem', $response); $testItem = $response['testItem']; $this->assertArrayHasKey('integer_list', $testItem); $this->assertEquals([4, 5, 6], $testItem['integer_list']); } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Mutation requests allowed only for POST requests + */ + public function testMutationIsNotAllowedViaGetRequest() + { + $id = 3; + + $query = <<<MUTATION +mutation { + testItem(id: {$id}) { + item_id, + name, + integer_list + } +} +MUTATION; + + $this->graphQlQuery($query, [], '', []); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php index d59e255daa109..2db06e383758f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php @@ -58,4 +58,46 @@ public function testQueryTestModuleExtensionAttribute() $this->assertArrayHasKey('integer_list', $testItem); $this->assertEquals([3, 4, 5], $testItem['integer_list']); } + + public function testQueryViaGetRequestReturnsResults() + { + $id = 1; + + $query = <<<QUERY +{ + testItem(id: {$id}) + { + item_id + name + } +} +QUERY; + + $response = $this->graphQlQuery($query, [], '', []); + + $this->assertArrayHasKey('testItem', $response); + } + + public function testQueryViaGetRequestWithVariablesReturnsResults() + { + $id = 1; + + $query = <<<QUERY +query getTestItem(\$id: Int!) +{ + testItem(id: \$id) + { + item_id + name + } +} +QUERY; + $variables = [ + "id" => $id + ]; + + $response = $this->graphQlQuery($query, $variables, '', []); + + $this->assertArrayHasKey('testItem', $response); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php new file mode 100644 index 0000000000000..fb0c205c86a2c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php @@ -0,0 +1,256 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Ups; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteShippingAddressIdByReservedQuoteId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting "UPS" shipping method on cart. Current class covers the next UPS shipping methods: + * + * | Code | Label + * -------------------------------------- + * | 1DM | Next Day Air Early AM + * | 1DA | Next Day Air + * | 2DA | 2nd Day Air + * | 3DS | 3 Day Select + * | GND | Ground + * | STD | Canada Standard + * | XPR | Worldwide Express + * | WXS | Worldwide Express Saver + * | XDM | Worldwide Express Plus + * | XPD | Worldwide Expedited + * + * Current class does not cover these UPS shipping methods (depends on address and sandbox settings) + * + * | Code | Label + * -------------------------------------- + * | 1DML | Next Day Air Early AM Letter + * | 1DAL | Next Day Air Letter + * | 1DAPI | Next Day Air Intra (Puerto Rico) + * | 1DP | Next Day Air Saver + * | 1DPL | Next Day Air Saver Letter + * | 2DM | 2nd Day Air AM + * | 2DML | 2nd Day Air AM Letter + * | 2DAL | 2nd Day Air Letter + * | GNDCOM | Ground Commercial + * | GNDRES | Ground Residential + * | XPRL | Worldwide Express Letter + * | XDML | Worldwide Express Plus Letter + */ +class SetUpsShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * Defines carrier label for "UPS" shipping method + */ + const CARRIER_LABEL = 'United Parcel Service'; + + /** + * Defines carrier code for "UPS" shipping method + */ + const CARRIER_CODE = 'ups'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteShippingAddressIdByReservedQuoteId + */ + private $getQuoteShippingAddressIdByReservedQuoteId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteShippingAddressIdByReservedQuoteId = $objectManager->get( + GetQuoteShippingAddressIdByReservedQuoteId::class + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php + * + * @dataProvider dataProviderShippingMethods + * @param string $methodCode + * @param string $methodLabel + */ + public function testSetUpsShippingMethod(string $methodCode, string $methodLabel) + { + $quoteReservedId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($quoteReservedId); + $shippingAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute($quoteReservedId); + + $query = $this->getQuery($maskedQuoteId, $shippingAddressId, self::CARRIER_CODE, $methodCode); + $response = $this->sendRequestWithToken($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals(self::CARRIER_CODE, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('label', $shippingAddress['selected_shipping_method']); + self::assertEquals( + self::CARRIER_LABEL . ' - ' . $methodLabel, + $shippingAddress['selected_shipping_method']['label'] + ); + } + + /** + * @return array + */ + public function dataProviderShippingMethods(): array + { + return [ + 'Next Day Air Early AM' => ['1DM', 'Next Day Air Early AM'], + 'Next Day Air' => ['1DA', 'Next Day Air'], + '2nd Day Air' => ['2DA', '2nd Day Air'], + '3 Day Select' => ['3DS', '3 Day Select'], + 'Ground' => ['GND', 'Ground'], + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php + * @magentoApiDataFixture Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php + * + * @dataProvider dataProviderShippingMethodsBasedOnCanadaAddress + * @param string $methodCode + * @param string $methodLabel + */ + public function testSetUpsShippingMethodBasedOnCanadaAddress(string $methodCode, string $methodLabel) + { + $quoteReservedId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($quoteReservedId); + $shippingAddressId = $this->getQuoteShippingAddressIdByReservedQuoteId->execute($quoteReservedId); + + $query = $this->getQuery($maskedQuoteId, $shippingAddressId, self::CARRIER_CODE, $methodCode); + $response = $this->sendRequestWithToken($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals(self::CARRIER_CODE, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('label', $shippingAddress['selected_shipping_method']); + self::assertEquals( + self::CARRIER_LABEL . ' - ' . $methodLabel, + $shippingAddress['selected_shipping_method']['label'] + ); + } + + /** + * @return array + */ + public function dataProviderShippingMethodsBasedOnCanadaAddress(): array + { + return [ + 'Canada Standard' => ['STD', 'Canada Standard'], + 'Worldwide Express' => ['XPR', 'Worldwide Express'], + 'Worldwide Express Saver' => ['WXS', 'Worldwide Express Saver'], + 'Worldwide Express Plus' => ['XDM', 'Worldwide Express Plus'], + 'Worldwide Expedited' => ['XPD', 'Worldwide Expedited'], + ]; + } + + /** + * Generates query for setting the specified shipping method on cart + * + * @param int $shippingAddressId + * @param string $maskedQuoteId + * @param string $carrierCode + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + int $shippingAddressId, + string $carrierCode, + string $methodCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "$maskedQuoteId" + shipping_methods: [ + { + cart_address_id: $shippingAddressId + carrier_code: "$carrierCode" + method_code: "$methodCode" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + label + } + } + } + } +} +QUERY; + } + + /** + * Sends a GraphQL request with using a bearer token + * + * @param string $query + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function sendRequestWithToken(string $query): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + + return $this->graphQlMutation($query, [], '', $headerMap); + } +} 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 c70b1631e85cd..370121a1dad78 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -31,7 +31,7 @@ protected function setUp() } /** - * Tests if target_path(canonical_url) is resolved for Product entity + * Tests if target_path(relative_url) is resolved for Product entity * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -60,7 +60,7 @@ public function testProductUrlResolver() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -68,12 +68,12 @@ public function testProductUrlResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } /** - * Tests the use case where canonical_url is provided as resolver input in the Query + * Tests the use case where relative_url is provided as resolver input in the Query * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -104,7 +104,7 @@ public function testProductUrlWithCanonicalUrlInput() urlResolver(url:"{$canonicalPath}") { id - canonical_url + relative_url type } } @@ -112,7 +112,7 @@ public function testProductUrlWithCanonicalUrlInput() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -147,7 +147,7 @@ public function testCategoryUrlResolver() urlResolver(url:"{$urlPath2}") { id - canonical_url + relative_url type } } @@ -155,7 +155,7 @@ public function testCategoryUrlResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -183,14 +183,14 @@ public function testCMSPageUrlResolver() urlResolver(url:"{$requestPath}") { id - canonical_url + relative_url type } } QUERY; $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); } @@ -226,7 +226,7 @@ public function testProductUrlRewriteResolver() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -234,7 +234,7 @@ public function testProductUrlRewriteResolver() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -266,7 +266,7 @@ public function testInvalidUrlResolverInput() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -307,7 +307,7 @@ public function testCategoryUrlWithLeadingSlash() urlResolver(url:"/{$urlPath}") { id - canonical_url + relative_url type } } @@ -315,7 +315,7 @@ public function testCategoryUrlWithLeadingSlash() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); } @@ -344,7 +344,7 @@ public function testResolveSlash() urlResolver(url:"/") { id - canonical_url + relative_url type } } @@ -352,7 +352,7 @@ public function testResolveSlash() $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['canonical_url']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php new file mode 100644 index 0000000000000..7448b165fc234 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\ProductRepositoryInterface; + +class VariablesSupportQueryTest extends GraphQlAbstract +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_list.php + */ + public function testQueryObjectVariablesSupport() + { + $productSku = 'simple-249'; + $minPrice = 153; + + $query + = <<<'QUERY' +query GetProductsQuery($pageSize: Int, $filterInput: ProductFilterInput, $priceSort: SortEnum) { + products( + pageSize: $pageSize + filter: $filterInput + sort: {price: $priceSort} + ) { + items { + sku + price { + minimalPrice { + amount { + value + currency + } + } + } + } + } +} +QUERY; + + $variables = [ + 'pageSize' => 1, + 'priceSort' => 'ASC', + 'filterInput' => [ + 'min_price' => [ + 'gt' => 150, + ], + ], + ]; + + $response = $this->graphQlQuery($query, $variables); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get($productSku, false, null, true); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertEquals(1, count($response['products']['items'])); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertEquals($product->getSku(), $response['products']['items'][0]['sku']); + self::assertEquals( + $minPrice, + $response['products']['items'][0]['price']['minimalPrice']['amount']['value'] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php new file mode 100644 index 0000000000000..45c82906d255d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Vault; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\Vault\Model\ResourceModel\PaymentToken as TokenResource; +use Magento\Vault\Model\ResourceModel\PaymentToken\CollectionFactory; + +/** + * Tests for customer payment tokens + */ +class CustomerPaymentTokensTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var PaymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @var CollectionFactory + */ + private $tokenCollectionFactory; + + /** + * @var TokenResource + */ + private $tokenResource; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->paymentTokenManagement = Bootstrap::getObjectManager()->get(PaymentTokenManagement::class); + $this->tokenResource = Bootstrap::getObjectManager()->get(TokenResource::class); + $this->tokenCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + protected function tearDown() + { + parent::tearDown(); + + $collection = $this->tokenCollectionFactory->create(); + $collection->addFieldToFilter('customer_id', ['eq' => 1]); + + foreach ($collection->getItems() as $token) { + // Using the resource directly to delete. Deleting from the repository only makes token inactive + $this->tokenResource->delete($token); + } + } + + /** + * @magentoApiDataFixture Magento/Vault/_files/payment_tokens.php + */ + public function testGetCustomerPaymentTokens() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +query { + customerPaymentTokens { + items { + public_hash + details + payment_method_code + type + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + + $this->assertEquals(2, count($response['customerPaymentTokens']['items'])); + $this->assertArrayHasKey('public_hash', $response['customerPaymentTokens']['items'][0]); + $this->assertArrayHasKey('details', $response['customerPaymentTokens']['items'][0]); + $this->assertArrayHasKey('payment_method_code', $response['customerPaymentTokens']['items'][0]); + $this->assertArrayHasKey('type', $response['customerPaymentTokens']['items'][0]); + // Validate gateway token is NOT returned + $this->assertArrayNotHasKey('gateway_token', $response['customerPaymentTokens']['items'][0]); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: The current customer isn't authorized. + */ + public function testGetCustomerPaymentTokensIfUserIsNotAuthorized() + { + $query = <<<QUERY +query { + customerPaymentTokens { + items { + public_hash + details + payment_method_code + type + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Vault/_files/payment_tokens.php + */ + public function testDeletePaymentToken() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $tokens = $this->paymentTokenManagement->getVisibleAvailableTokens(1); + $token = current($tokens); + $publicHash = $token->getPublicHash(); + + $query = <<<QUERY +mutation { + deletePaymentToken( + public_hash: "$publicHash" + ) { + result + customerPaymentTokens { + items { + public_hash + details + payment_method_code + type + } + } + } +} +QUERY; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertTrue($response['deletePaymentToken']['result']); + $this->assertEquals(1, count($response['deletePaymentToken']['customerPaymentTokens']['items'])); + + $token = $response['deletePaymentToken']['customerPaymentTokens']['items'][0]; + $this->assertArrayHasKey('public_hash', $token); + $this->assertArrayHasKey('details', $token); + $this->assertArrayHasKey('payment_method_code', $token); + $this->assertArrayHasKey('type', $token); + // Validate gateway token is NOT returned + $this->assertArrayNotHasKey('gateway_token', $token); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: The current customer isn't authorized. + */ + public function testDeletePaymentTokenIfUserIsNotAuthorized() + { + $query = <<<QUERY +mutation { + deletePaymentToken( + public_hash: "ksdfk392ks" + ) { + result + } +} +QUERY; + $this->graphQlMutation($query, [], ''); + } + + /** + * @magentoApiDataFixture Magento/Vault/_files/payment_tokens.php + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: Could not find a token using public hash: ksdfk392ks + */ + public function testDeletePaymentTokenInvalidPublicHash() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + deletePaymentToken( + public_hash: "ksdfk392ks" + ) { + result + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @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]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php index d570fc09b7714..4aac5d9445934 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -93,6 +93,36 @@ public function testGetCustomerWishlist(): void $this->assertEquals($wishlistItemProduct->getName(), $response['wishlist']['items'][0]['product']['name']); } + /** + * @expectedException \Exception + * @expectedExceptionMessage The current user cannot perform operations on wishlist + */ + public function testGetGuestWishlist() + { + $query = + <<<QUERY +{ + wishlist { + items_count + name + sharing_code + updated_at + items { + id + qty + description + added_at + product { + sku + name + } + } + } +} +QUERY; + $this->graphQlQuery($query); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php index a001cae645434..a3ded4f5f125c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php @@ -12,6 +12,8 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; class CartTotalRepositoryTest extends WebapiAbstract { @@ -54,36 +56,11 @@ public function testGetTotals() /** @var \Magento\Quote\Model\Quote\Address $shippingAddress */ $shippingAddress = $quote->getShippingAddress(); - $data = [ - Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), - Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), - Totals::KEY_SUBTOTAL => $quote->getSubtotal(), - Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), - Totals::KEY_DISCOUNT_AMOUNT => $shippingAddress->getDiscountAmount(), - Totals::KEY_BASE_DISCOUNT_AMOUNT => $shippingAddress->getBaseDiscountAmount(), - Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), - Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), - Totals::KEY_SHIPPING_AMOUNT => $shippingAddress->getShippingAmount(), - Totals::KEY_BASE_SHIPPING_AMOUNT => $shippingAddress->getBaseShippingAmount(), - Totals::KEY_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getShippingDiscountAmount(), - Totals::KEY_BASE_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getBaseShippingDiscountAmount(), - Totals::KEY_TAX_AMOUNT => $shippingAddress->getTaxAmount(), - Totals::KEY_BASE_TAX_AMOUNT => $shippingAddress->getBaseTaxAmount(), - Totals::KEY_SHIPPING_TAX_AMOUNT => $shippingAddress->getShippingTaxAmount(), - Totals::KEY_BASE_SHIPPING_TAX_AMOUNT => $shippingAddress->getBaseShippingTaxAmount(), - Totals::KEY_SUBTOTAL_INCL_TAX => $shippingAddress->getSubtotalInclTax(), - Totals::KEY_BASE_SUBTOTAL_INCL_TAX => $shippingAddress->getBaseSubtotalTotalInclTax(), - Totals::KEY_SHIPPING_INCL_TAX => $shippingAddress->getShippingInclTax(), - Totals::KEY_BASE_SHIPPING_INCL_TAX => $shippingAddress->getBaseShippingInclTax(), - Totals::KEY_BASE_CURRENCY_CODE => $quote->getBaseCurrencyCode(), - Totals::KEY_QUOTE_CURRENCY_CODE => $quote->getQuoteCurrencyCode(), - Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), - Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], - ]; + $data = $this->getData($quote, $shippingAddress); + $data = $this->formatTotalsData($data); $requestData = ['cartId' => $cartId]; - $data = $this->formatTotalsData($data); $actual = $this->_webApiCall($this->getServiceInfoForTotalsService($cartId), $requestData); unset($actual['items'][0]['options']); unset($actual['weee_tax_applied_amount']); @@ -213,7 +190,32 @@ public function testGetMyTotals() /** @var \Magento\Quote\Model\Quote\Address $shippingAddress */ $shippingAddress = $quote->getShippingAddress(); - $data = [ + $data = $this->getData($quote, $shippingAddress); + $data = $this->formatTotalsData($data); + + $actual = $this->_webApiCall($serviceInfo); + unset($actual['items'][0]['options']); + unset($actual['weee_tax_applied_amount']); + + /** TODO: cover total segments with separate test */ + unset($actual['total_segments']); + if (array_key_exists('extension_attributes', $actual)) { + unset($actual['extension_attributes']); + } + $this->assertEquals($data, $actual); + } + + /** + * Get expected data. + * + * @param Quote $quote + * @param Address $shippingAddress + * + * @return array + */ + private function getData(Quote $quote, Address $shippingAddress) : array + { + return [ Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_SUBTOTAL => $quote->getSubtotal(), @@ -239,17 +241,5 @@ public function testGetMyTotals() Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], ]; - - $data = $this->formatTotalsData($data); - $actual = $this->_webApiCall($serviceInfo); - unset($actual['items'][0]['options']); - unset($actual['weee_tax_applied_amount']); - - /** TODO: cover total segments with separate test */ - unset($actual['total_segments']); - if (array_key_exists('extension_attributes', $actual)) { - unset($actual['extension_attributes']); - } - $this->assertEquals($data, $actual); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php index 1aee493d8e0cb..1fb8fc43b0db6 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php @@ -9,6 +9,9 @@ use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Coupon management service tests + */ class CouponManagementTest extends WebapiAbstract { const SERVICE_VERSION = 'V1'; @@ -93,7 +96,7 @@ public function testSetCouponThrowsExceptionIfCouponDoesNotExist() $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, ], 'soap' => [ @@ -129,7 +132,7 @@ public function testSetCouponSuccess() $couponCode = $salesRule->getPrimaryCoupon()->getCode(); $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, ], 'soap' => [ @@ -232,7 +235,7 @@ public function testSetMyCouponThrowsExceptionIfCouponDoesNotExist() $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $token, ], @@ -280,7 +283,7 @@ public function testSetMyCouponSuccess() $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $token, ], diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php index 7ad0e62f29dc3..28195cca679f8 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php @@ -63,14 +63,14 @@ public function testGetTotals() $shippingAddress = $quote->getShippingAddress(); $data = [ - Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), - Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), + Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_SUBTOTAL => $quote->getSubtotal(), - Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), - Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), + Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), Totals::KEY_DISCOUNT_AMOUNT => $shippingAddress->getDiscountAmount(), Totals::KEY_BASE_DISCOUNT_AMOUNT => $shippingAddress->getBaseDiscountAmount(), + Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), + Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), Totals::KEY_SHIPPING_AMOUNT => $shippingAddress->getShippingAmount(), Totals::KEY_BASE_SHIPPING_AMOUNT => $shippingAddress->getBaseShippingAmount(), Totals::KEY_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getShippingDiscountAmount(), @@ -94,6 +94,7 @@ public function testGetTotals() $data = $this->formatTotalsData($data); $actual = $this->_webApiCall($this->getServiceInfoForTotalsService($cartId), $requestData); + $actual = $this->formatTotalsData($actual); unset($actual['items'][0]['options']); unset($actual['weee_tax_applied_amount']); diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php index 09c49bed1de83..db96728e206be 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php @@ -133,6 +133,8 @@ public function testOrderGetExtensionAttributes(): void self::assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); self::assertNotEmpty($appliedTaxes[0]['applied_taxes']); self::assertEquals(true, $result['extension_attributes']['converting_from_quote']); + self::assertArrayHasKey('payment_additional_info', $result['extension_attributes']); + self::assertNotEmpty($result['extension_attributes']['payment_additional_info']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php index 592bdf3d584a9..9ba648c73276b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php @@ -106,5 +106,7 @@ public function testGetOrderWithDiscount() $this->assertTrue(is_array($response)); $this->assertEquals(8.00, $response['row_total']); $this->assertEquals(8.00, $response['base_row_total']); + $this->assertEquals(9.00, $response['row_total_incl_tax']); + $this->assertEquals(9.00, $response['base_row_total_incl_tax']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php index 5050b6be7e56c..506f82eab7ae2 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php @@ -90,6 +90,8 @@ public function testOrderListExtensionAttributes() $this->assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); $this->assertNotEmpty($appliedTaxes[0]['applied_taxes']); $this->assertEquals(true, $result['items'][0]['extension_attributes']['converting_from_quote']); + $this->assertArrayHasKey('payment_additional_info', $result['items'][0]['extension_attributes']); + $this->assertNotEmpty($result['items'][0]['extension_attributes']['payment_additional_info']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php index c5ecead00ce29..9e3bd4ca48478 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php @@ -11,6 +11,7 @@ /** * Class OrderCommentAddTest + * * @package Magento\Sales\Service\V1 */ class OrderStatusHistoryAddTest extends WebapiAbstract @@ -48,7 +49,7 @@ public function testOrderCommentAdd() OrderStatusHistoryInterface::CREATED_AT => null, OrderStatusHistoryInterface::PARENT_ID => $order->getId(), OrderStatusHistoryInterface::ENTITY_NAME => null, - OrderStatusHistoryInterface::STATUS => null, + OrderStatusHistoryInterface::STATUS => $order->getStatus(), OrderStatusHistoryInterface::IS_VISIBLE_ON_FRONT => 1, ]; @@ -69,25 +70,27 @@ public function testOrderCommentAdd() //Verification $comments = $order->load($order->getId())->getAllStatusHistory(); + $comment = reset($comments); - $commentData = reset($comments); - foreach ($commentData as $key => $value) { - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::COMMENT], - $statusHistoryComment->getComment() - ); - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::PARENT_ID], - $statusHistoryComment->getParentId() - ); - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::IS_CUSTOMER_NOTIFIED], - $statusHistoryComment->getIsCustomerNotified() - ); - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::IS_VISIBLE_ON_FRONT], - $statusHistoryComment->getIsVisibleOnFront() - ); - } + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::COMMENT], + $comment->getComment() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::PARENT_ID], + $comment->getParentId() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::IS_CUSTOMER_NOTIFIED], + $comment->getIsCustomerNotified() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::IS_VISIBLE_ON_FRONT], + $comment->getIsVisibleOnFront() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::STATUS], + $comment->getStatus() + ); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php index 8e5373ea76576..92942d7acc6f2 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Service\V1; use Magento\Sales\Model\Order; @@ -201,6 +202,73 @@ public function testFullRequest() } } + /** + * Test order will keep same(custom) status after partial refund, if state has not been changed. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusPartialRefund() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $items = $this->getOrderItems($existingOrder); + $items[0]['qty'] -= 1; + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $items, + ] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('custom_processing', $updatedOrder->getStatus()); + $this->assertSame('processing', $updatedOrder->getState()); + } + + /** + * Test order will change custom status after total refund, when state has been changed. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusTotalRefund() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $items = $this->getOrderItems($existingOrder); + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $items, + ] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('complete', $updatedOrder->getStatus()); + $this->assertSame('complete', $updatedOrder->getState()); + } + /** * Prepares and returns info for API service. * diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php index 5f967758e21bd..8beb14e81be71 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php @@ -6,12 +6,14 @@ namespace Magento\Webapi; +use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\Api\SortOrder; -use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SortOrderBuilder; +/** + * Test join directives. + */ class JoinDirectivesTest extends \Magento\TestFramework\TestCase\WebapiAbstract { /** @@ -44,7 +46,8 @@ protected function setUp() } /** - * Rollback rules + * Rollback rules. + * * @magentoApiDataFixture Magento/SalesRule/_files/rules_rollback.php * @magentoApiDataFixture Magento/Sales/_files/quote.php */ @@ -124,6 +127,49 @@ public function testAutoGeneratedGetList() $this->assertEquals($expectedExtensionAttributes['email'], $testAttribute['email']); } + /** + * Test get list of orders with extension attributes. + * + * @magentoApiDataFixture Magento/Sales/_files/order.php + */ + public function testGetOrdertList() + { + $filter = $this->filterBuilder + ->setField('increment_id') + ->setValue('100000001') + ->setConditionType('eq') + ->create(); + $this->searchBuilder->addFilters([$filter]); + $searchData = $this->searchBuilder->create()->__toArray(); + + $requestData = ['searchCriteria' => $searchData]; + + $restResourcePath = '/V1/orders/'; + $soapService = 'salesOrderRepositoryV1'; + $expectedExtensionAttributes = $this->getExpectedExtensionAttributes(); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $restResourcePath . '?' . http_build_query($requestData), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => $soapService, + 'operation' => $soapService . 'GetList', + ], + ]; + $searchResult = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertArrayHasKey('items', $searchResult); + $itemData = array_pop($searchResult['items']); + $this->assertArrayHasKey('extension_attributes', $itemData); + $this->assertArrayHasKey('order_api_test_attribute', $itemData['extension_attributes']); + $testAttribute = $itemData['extension_attributes']['order_api_test_attribute']; + $this->assertEquals($expectedExtensionAttributes['firstname'], $testAttribute['first_name']); + $this->assertEquals($expectedExtensionAttributes['lastname'], $testAttribute['last_name']); + $this->assertEquals($expectedExtensionAttributes['email'], $testAttribute['email']); + } + /** * Retrieve the admin user's information. * diff --git a/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php b/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php index 60abe18f5a7ba..fc54e73ff1ac2 100644 --- a/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php +++ b/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php @@ -7,6 +7,7 @@ namespace Magento\Mtf\App\State; use Magento\Mtf\ObjectManager; +use Magento\Mtf\Util\Command\Cli; use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; @@ -27,7 +28,7 @@ class State1 extends AbstractState * * @var string */ - protected $config ='admin_session_lifetime_1_hour, wysiwyg_disabled, admin_account_sharing_enable, log_to_file'; + protected $config ='admin_session_lifetime_1_hour, wysiwyg_disabled, admin_account_sharing_enable'; /** * HTTP CURL Adapter. @@ -55,6 +56,7 @@ public function __construct( * Apply set up configuration profile. * * @return void + * @throws \Exception */ public function apply() { @@ -67,6 +69,10 @@ public function apply() ['configData' => $this->config] )->run(); } + + /** @var Cli $cli */ + $cli = $this->objectManager->create(Cli::class); + $cli->execute('setup:config:set', ['--enable-debug-logging=true']); } /** diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php index d1fd351302414..03492f7ae1a9e 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php @@ -6,9 +6,9 @@ namespace Magento\Mtf\Client\Element; -use Magento\Mtf\ObjectManager; -use Magento\Mtf\Client\Locator; use Magento\Mtf\Client\ElementInterface; +use Magento\Mtf\Client\Locator; +use Magento\Mtf\ObjectManager; /** * Typified element class for conditions. @@ -135,6 +135,13 @@ class ConditionsElement extends SimpleElement */ protected $chooserGridLocator = 'div[id*=chooser]'; + /** + * Datepicker xpath. + * + * @var string + */ + private $datepicker = './/*[contains(@class,"ui-datepicker-trigger")]'; + /** * Key of last find param. * @@ -189,10 +196,14 @@ class ConditionsElement extends SimpleElement protected $exception; /** - * Set value to conditions. + * Condition option text selector. * - * @param string $value - * @return void + * @var string + */ + private $conditionOptionTextSelector = '//option[normalize-space(text())="%s"]'; + + /** + * @inheritdoc */ public function setValue($value) { @@ -261,7 +272,7 @@ protected function addSingleCondition($condition, ElementInterface $context) $this->addCondition($condition['type'], $context); $createdCondition = $context->find($this->created, Locator::SELECTOR_XPATH); $this->waitForCondition($createdCondition); - $this->fillCondition($condition['rules'], $createdCondition); + $this->fillCondition($condition['rules'], $createdCondition, $condition['type']); } /** @@ -278,10 +289,16 @@ protected function addCondition($type, ElementInterface $context) $count = 0; do { - $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); - try { - $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select')->setValue($type); + $specificType = $newCondition->find( + sprintf($this->conditionOptionTextSelector, $type), + Locator::SELECTOR_XPATH + )->isPresent(); + $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); + $condition = $specificType + ? $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'selectcondition') + : $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select'); + $condition->setValue($type); $isSetType = true; } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { $isSetType = false; @@ -302,13 +319,14 @@ protected function addCondition($type, ElementInterface $context) * * @param array $rules * @param ElementInterface $element + * @param string|null $type * @return void * @throws \Exception * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function fillCondition(array $rules, ElementInterface $element) + protected function fillCondition(array $rules, ElementInterface $element, $type = null) { $this->resetKeyParam(); foreach ($rules as $rule) { @@ -329,7 +347,7 @@ protected function fillCondition(array $rules, ElementInterface $element) if ($this->fillGrid($rule, $param)) { $isSet = true; - } elseif ($this->fillSelect($rule, $param)) { + } elseif ($this->fillSelect($rule, $param, $type)) { $isSet = true; } elseif ($this->fillText($rule, $param)) { $isSet = true; @@ -386,11 +404,15 @@ protected function fillGrid($rule, ElementInterface $param) * * @param string $rule * @param ElementInterface $param + * @param string|null $type * @return bool */ - protected function fillSelect($rule, ElementInterface $param) + protected function fillSelect($rule, ElementInterface $param, $type = null) { - $value = $param->find('select', Locator::SELECTOR_TAG_NAME, 'select'); + //Avoid confusion between regions like: "Baja California" and "California". + $value = strpos($type, 'State/Province') === false + ? $param->find('select', Locator::SELECTOR_TAG_NAME, 'select') + : $param->find('select', Locator::SELECTOR_TAG_NAME, 'selectstate'); if ($value->isVisible()) { $value->setValue($rule); $this->click(); @@ -411,7 +433,16 @@ protected function fillText($rule, ElementInterface $param) { $value = $param->find('input', Locator::SELECTOR_TAG_NAME); if ($value->isVisible()) { - $value->setValue($rule); + if (!$value->getAttribute('readonly')) { + $value->setValue($rule); + } else { + $datepicker = $param->find( + $this->datepicker, + Locator::SELECTOR_XPATH, + DatepickerElement::class + ); + $datepicker->setValue($rule); + } $apply = $param->find('.//*[@class="rule-param-apply"]', Locator::SELECTOR_XPATH); if ($apply->isVisible()) { diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php index a0e350cb3da43..eb277c2cc43dd 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php @@ -66,13 +66,16 @@ public function setValue($value) $date = $this->parseDate($value); $date[1] = ltrim($date[1], '0'); $this->click(); - $this->find($this->datePickerButton, Locator::SELECTOR_XPATH)->click(); $datapicker = $this->find($this->datePickerBlock, Locator::SELECTOR_XPATH); + $datepickerClose = $datapicker->find($this->datePickerButtonClose, Locator::SELECTOR_XPATH); + if (!$datepickerClose->isVisible()) { + $this->find($this->datePickerButton, Locator::SELECTOR_XPATH)->click(); + } $datapicker->find($this->datePickerYear, Locator::SELECTOR_XPATH, 'select')->setValue($date[2]); $datapicker->find($this->datePickerMonth, Locator::SELECTOR_XPATH, 'select')->setValue($date[0]); $datapicker->find(sprintf($this->datePickerCalendar, $date[1]), Locator::SELECTOR_XPATH)->click(); if ($datapicker->isVisible()) { - $datapicker->find($this->datePickerButtonClose, Locator::SELECTOR_XPATH)->click(); + $datepickerClose->click(); } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php new file mode 100644 index 0000000000000..15a799eac5188 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectconditionElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectstateElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectstateElement.php new file mode 100644 index 0000000000000..a21353f46c1ca --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectstateElement.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectstateElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php b/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php index 745f97b9b43ff..836cc486cb0ff 100644 --- a/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php +++ b/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Mtf\EntryPoint; @@ -10,8 +11,8 @@ /** * Class EntryPoint - * Application entry point, used to bootstrap and run application * + * Application entry point, used to bootstrap and run application */ class EntryPoint { @@ -36,7 +37,6 @@ class EntryPoint * @param string $rootDir * @param array $parameters * @param ObjectManager $objectManager - * @SuppressWarnings(PHPMD.ExitExpression) */ public function __construct( $rootDir, @@ -51,7 +51,7 @@ public function __construct( /** * Run a Mtf application * - * @param $applicationName + * @param string $applicationName * @param array $arguments * @return mixed * @throws \DomainException diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php index f2ab1501dc2ba..1dac1f213920e 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php @@ -9,6 +9,7 @@ use Magento\Mtf\ObjectManagerInterface; use Magento\Mtf\Util\Command\File\Export\Data; use Magento\Mtf\Util\Command\File\Export\ReaderInterface; +use Magento\ImportExport\Test\Page\Adminhtml\AdminExportIndex; /** * Get Exporting file from the Magento. @@ -36,13 +37,26 @@ class Export implements ExportInterface */ private $reader; + /** + * Admin export index page. + * + * @var AdminExportIndex + */ + private $adminExportIndex; + /** * @param ObjectManagerInterface $objectManager - * @param string $type [optional] + * @param AdminExportIndex $adminExportIndex + * @param string $type + * @throws \ReflectionException */ - public function __construct(ObjectManagerInterface $objectManager, $type = 'product') - { + public function __construct( + ObjectManagerInterface $objectManager, + AdminExportIndex $adminExportIndex, + $type = 'product' + ) { $this->objectManager = $objectManager; + $this->adminExportIndex = $adminExportIndex; $this->reader = $this->getReader($type); } @@ -68,9 +82,11 @@ private function getReader($type) * * @param string $name * @return Data|null + * @throws \Exception */ public function getByName($name) { + $this->downloadFile(); $this->reader->getData(); foreach ($this->reader->getData() as $file) { if ($file->getName() === $name) { @@ -85,9 +101,11 @@ public function getByName($name) * Get latest created the export file. * * @return Data|null + * @throws \Exception */ public function getLatest() { + $this->downloadFile(); $max = 0; $latest = null; foreach ($this->reader->getData() as $file) { @@ -106,9 +124,11 @@ public function getLatest() * @param string $start * @param string $end * @return Data[] + * @throws \Exception */ public function getByDateRange($start, $end) { + $this->downloadFile(); $files = []; foreach ($this->reader->getData() as $file) { if ($file->getDate() > $start && $file->getDate() < $end) { @@ -123,9 +143,25 @@ public function getByDateRange($start, $end) * Get all export files. * * @return Data[] + * @throws \Exception */ public function getAll() { + $this->downloadFile(); return $this->reader->getData(); } + + /** + * Download exported file + * + * @return void + * @throws \Exception + */ + private function downloadFile() + { + $this->adminExportIndex->open(); + /** @var \Magento\ImportExport\Test\Block\Adminhtml\Export\ExportedGrid $exportedGrid */ + $exportedGrid = $this->adminExportIndex->getExportedGrid(); + $exportedGrid->downloadFirstFile(); + } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php index 1c05fbaebf625..d7336b51a18e2 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php @@ -73,8 +73,8 @@ private function getFiles() $this->transport->write($this->prepareUrl(), [], CurlInterface::GET); $serializedFiles = $this->transport->read(); $this->transport->close(); - - return unserialize($serializedFiles); + // phpcs:ignore Magento2.Security.InsecureFunction + return unserialize($serializedFiles, ['allowed_classes' => false]); } /** diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php index 8b41924fe0a90..f4e55682857a2 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php @@ -45,7 +45,7 @@ public function getFileContent($name) $curl->write($this->prepareUrl($name), [], CurlTransport::GET); $data = $curl->read(); $curl->close(); - + // phpcs:ignore Magento2.Security.InsecureFunction return unserialize($data); } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php index bb82715e4b402..b1c552370835c 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php @@ -77,6 +77,7 @@ protected function authorize() $this->transport->write($url, $data, CurlInterface::POST); $response = $this->read(); if (strpos($response, 'login-form') !== false) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'Admin user cannot be logged in by curl handler!' ); @@ -111,6 +112,7 @@ public function write($url, $params = [], $method = CurlInterface::POST, $header if ($this->formKey) { $params['form_key'] = $this->formKey; } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception(sprintf('Form key is absent! Url: "%s" Response: "%s"', $url, $this->response)); } $this->transport->write($url, http_build_query($params), $method, $headers); diff --git a/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml index f2f56eb74f704..99bd9c6d9d220 100644 --- a/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Notifications</data> <data name="pageTitle" xsi:type="string">Notifications</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/Constraint/AssertExportAdvancedPricing.php b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/Constraint/AssertExportAdvancedPricing.php index 565d0f432bdaf..c92563c1ca5bd 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/Constraint/AssertExportAdvancedPricing.php +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/Constraint/AssertExportAdvancedPricing.php @@ -8,6 +8,7 @@ use Magento\Mtf\Constraint\AbstractConstraint; use Magento\Mtf\Fixture\InjectableFixture; use Magento\Mtf\Util\Command\File\ExportInterface; +use Magento\ImportExport\Test\Page\Adminhtml\AdminExportIndex; /** * Assert that exported file with advanced pricing options contains product data. @@ -21,19 +22,30 @@ class AssertExportAdvancedPricing extends AbstractConstraint */ private $exportData; + /** + * Admin export index page. + * + * @var AdminExportIndex + */ + private $adminExportIndex; + /** * Assert that exported file with advanced pricing options contains product data. * * @param ExportInterface $export * @param array $products * @param array $exportedFields + * @param AdminExportIndex $adminExportIndex * @return void */ public function processAssert( ExportInterface $export, array $products, - array $exportedFields + array $exportedFields, + AdminExportIndex $adminExportIndex ) { + $this->adminExportIndex = $adminExportIndex; + $this->adminExportIndex->open(); $this->exportData = $export->getLatest(); foreach ($products as $product) { $regexps = $this->prepareRegexpsForCheck($exportedFields, $product); diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php index df8cd6f354c2a..c2c684c89d06b 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php @@ -11,6 +11,7 @@ use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; use Magento\Store\Test\Fixture\Website; +use Magento\Mtf\Util\Command\Cli\Cron; /** * Preconditions: @@ -65,16 +66,23 @@ class ExportAdvancedPricingTest extends Injectable private $catalogProductIndex; /** - * Prepare test data. + * Cron command * - * @param CatalogProductIndex $catalogProductIndex + * @var Cron + */ + private $cron; + + /** + * Run cron before tests running + * + * @param Cron $cron * @return void */ public function __prepare( - CatalogProductIndex $catalogProductIndex + Cron $cron ) { - $catalogProductIndex->open(); - $catalogProductIndex->getProductGrid()->massaction([], 'Delete', true, 'Select All'); + $cron->run(); + $cron->run(); } /** @@ -84,18 +92,21 @@ public function __prepare( * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex * @param CatalogProductIndex $catalogProductIndexPage + * @param Cron $cron * @return void */ public function __inject( TestStepFactory $stepFactory, FixtureFactory $fixtureFactory, AdminExportIndex $adminExportIndex, - CatalogProductIndex $catalogProductIndexPage + CatalogProductIndex $catalogProductIndexPage, + Cron $cron ) { $this->stepFactory = $stepFactory; $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; $this->catalogProductIndex = $catalogProductIndexPage; + $this->cron = $cron; } /** @@ -130,9 +141,13 @@ public function test( $website->persist(); $this->setupCurrencyForCustomWebsite($website, $currencyCustomWebsite); } + $this->cron->run(); + $this->cron->run(); $products = $this->prepareProducts($products, $website); + $this->cron->run(); + $this->cron->run(); $this->adminExportIndex->open(); - + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData = $this->fixtureFactory->createByCode( 'exportData', [ @@ -150,7 +165,8 @@ public function test( if (!empty($advancedPricingAttributes)) { $products = [$products[0]]; } - + $this->cron->run(); + $this->cron->run(); return [ 'products' => $products ]; @@ -191,6 +207,9 @@ private function setupCurrencyForCustomWebsite($website, $currencyDataset) */ public function prepareProducts(array $products, Website $website = null) { + $this->catalogProductIndex->open(); + $this->catalogProductIndex->getProductGrid()->massaction([], 'Delete', true, 'Select All'); + if (empty($products)) { return null; } diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml index 9f19ff4cb00a8..07646c2aceda8 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml @@ -9,6 +9,7 @@ <testCase name="Magento\AdvancedPricingImportExport\Test\TestCase\ExportAdvancedPricingTest" summary="Export with advanced pricing entity type option"> <variation name="ExportAdvancedPricingTestVariation1" summary="Trying export product data with advanced pricing option but without created products" ticketId="MAGETWO-46147"> <data name="exportData" xsi:type="string">csv_with_advanced_pricing</data> + <constraint name="Magento\ImportExport\Test\Constraint\AssertExportSubmittedMessage"/> <constraint name="Magento\ImportExport\Test\Constraint\AssertExportNoDataErrorMessage"/> </variation> <variation name="ExportAdvancedPricingTestVariation2" summary="Trying export product data with advanced pricing option" ticketId="MAGETWO-46120"> diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ImportDataNegativeTest.xml b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ImportDataNegativeTest.xml index 65b4d6e973bb3..db992e662d817 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ImportDataNegativeTest.xml +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ImportDataNegativeTest.xml @@ -19,7 +19,7 @@ <item name="entity" xsi:type="string">Advanced Pricing</item> <item name="behavior" xsi:type="string">Add/Update</item> <item name="validation_strategy" xsi:type="string">Stop on Error</item> - <item name="allowed_error_count" xsi:type="string">10</item> + <item name="allowed_error_count" xsi:type="string">1</item> <item name="import_field_separator" xsi:type="string">,</item> <item name="import_multiple_value_separator" xsi:type="string">,</item> <item name="import_file" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/_files/template/pricing/advanced_incorrect.php b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/_files/template/pricing/advanced_incorrect.php index 12203222534cd..e728a87616392 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/_files/template/pricing/advanced_incorrect.php +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/_files/template/pricing/advanced_incorrect.php @@ -14,5 +14,13 @@ 'tier_price' => 'text', 'tier_price_value_type' => 'Fixed', ], + 'data_1' => [ + 'sku' => '%sku%', + 'tier_price_website' => "All Websites [USD]", + 'tier_price_customer_group' => 'ALL GROUPS', + 'tier_price_qty' => '3', + 'tier_price' => 'text', + 'tier_price_value_type' => 'Fixed', + ], ], ]; diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/AdvancedReportingButtonTest.xml b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/AdvancedReportingButtonTest.xml index a975d19ef8879..89c9d9168921f 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/AdvancedReportingButtonTest.xml +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/AdvancedReportingButtonTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Analytics\Test\TestCase\AdvancedReportingButtonTest" summary="Navigate through Advanced Reporting button on dashboard to Sign Up page" ticketId="MAGETWO-63715"> <variation name="AdvancedReportingButtonTest"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="advancedReportingLink" xsi:type="string">https://advancedreporting.rjmetrics.com/report</data> <constraint name="Magento\Analytics\Test\Constraint\AssertAdvancedReportingPage" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml index cdb73c5d36f25..8f7b07c8c14c4 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate to menu chapter"> <variation name="NavigateMenuTestBIEssentials" summary="Navigate through BI Essentials admin menu to Sign Up page" ticketId="MAGETWO-63700"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="issue" xsi:type="string">MAGETWO-97261: Magento\Backend\Test\TestCase\NavigateMenuTest fails on Jenkins</data> <data name="menuItem" xsi:type="string">Reports > BI Essentials</data> <data name="waitMenuItemNotVisible" xsi:type="boolean">false</data> @@ -15,6 +16,7 @@ <constraint name="Magento\Analytics\Test\Constraint\AssertBIEssentialsLink" /> </variation> <variation name="NavigateMenuTestAdvancedReporting" summary="Navigate through Advanced Reporting admin menu to BI Reports page" ticketId="MAGETWO-65748"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Advanced Reporting</data> <data name="waitMenuItemNotVisible" xsi:type="boolean">false</data> <data name="advancedReportingLink" xsi:type="string">https://advancedreporting.rjmetrics.com/report</data> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml index adae65a1d06d6..799f9e30fd972 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml @@ -17,5 +17,6 @@ <block name="accessDeniedBlock" class="Magento\Backend\Test\Block\Denied" locator="#anchor-content" strategy="css selector" /> <block name="systemMessageDialog" class="Magento\AdminNotification\Test\Block\System\Messages" locator='.ui-popup-message .modal-inner-wrap' strategy="css selector" /> <block name="applicationVersion" class="Magento\Backend\Test\Block\Version" locator="body" strategy="css selector" /> + <block name="modalMessage" class="Magento\Ui\Test\Block\Adminhtml\Modal" locator=".modal-popup>.modal-inner-wrap" strategy="css selector" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml index 1792ddb5abdc9..9985e962b04eb 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml @@ -174,15 +174,6 @@ <item name="value" xsi:type="number">0</item> </field> </dataset> - - <dataset name="log_to_file"> - <field name="dev/debug/debug_logging" xsi:type="array"> - <item name="scope" xsi:type="string">default</item> - <item name="scope_id" xsi:type="number">0</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - </dataset> <dataset name="minify_js_files"> <field name="dev/js/minify_files" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml index dca6e1d15024f..6d9c50b4317c8 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml @@ -8,31 +8,26 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\GlobalSearchEntityTest" summary="Global Search Backend " ticketId="MAGETWO-28457"> <variation name="GlobalSearchEntityTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search shows admin preview</data> <data name="search/data/query" xsi:type="string">Some search term</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchPreview" /> </variation> <variation name="GlobalSearchEntityTestVariation2"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search with 2 sign return no results</data> <data name="search/data/query" xsi:type="string">:)</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchNoRecordsFound" /> </variation> <variation name="GlobalSearchEntityTestVariation3"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search product by sku</data> <data name="search/data/query" xsi:type="string">orderInjectable::default::product::sku</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchProductName" /> </variation> <variation name="GlobalSearchEntityTestVariation4"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search existed customer</data> <data name="search/data/query" xsi:type="string">customer::johndoe_unique::lastname</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchCustomerName" /> </variation> <variation name="GlobalSearchEntityTestVariation5"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search order (by order id)</data> <data name="search/data/query" xsi:type="string">orderInjectable::default::id</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchOrderId" /> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php index 1c3d018af077a..4a6202f815b92 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php @@ -9,6 +9,7 @@ use Magento\Backend\Test\Page\Adminhtml\Dashboard; use Magento\Mtf\Util\Command\Cli\DeployMode; use Magento\Mtf\TestStep\TestStepFactory; +use Magento\User\Test\TestStep\LoginUserOnBackendStep; /** * Verify visibility of form elements on Configuration page. @@ -53,9 +54,11 @@ public function __inject( } /** - * Admin login test after JS minification is turned on in production mode + * Admin login test after JS minification is turned on in production mode. + * * @param DeployMode $cli * @param null $configData + * * @return void */ public function test( @@ -64,15 +67,26 @@ public function test( ) { $this->configData = $configData; - //Pre-conditions + //Pre-conditions $cli->setDeployModeToDeveloper(); - $this->objectManager->create( + $this->stepFactory->create( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $this->configData] )->run(); // Steps $cli->setDeployModeToProduction(); - $this->adminDashboardPage->open(); + $this->stepFactory->create(LoginUserOnBackendStep::class)->run(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->stepFactory->create( + \Magento\Config\Test\TestStep\SetupConfigurationStep::class, + ['configData' => $this->configData] + )->cleanup(); } } diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml index 67842f62d7c92..afdf70704a984 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml @@ -8,26 +8,31 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate through admin menu" ticketId="MAGETWO-34874"> <variation name="NavigateMenuTest2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Dashboard</data> <data name="pageTitle" xsi:type="string">Dashboard</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Schedule</data> <data name="pageTitle" xsi:type="string">Store Design Schedule</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > All Stores</data> <data name="pageTitle" xsi:type="string">Stores</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Configuration</data> <data name="pageTitle" xsi:type="string">Configuration</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Cache Management</data> <data name="pageTitle" xsi:type="string">Cache Management</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php index 9b12c467e5775..4d6d06ac6e625 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php @@ -46,7 +46,7 @@ public function getData($key = null) $optionData = [ 'title' => $checkoutOption['title'], 'value' => "{$qty} x {$value} {$price}", - 'sku' => "{$qty} x {$value}" + 'sku' => "{$value}" ]; $checkoutBundleOptions[$checkoutOptionKey] = $optionData; diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml index 45e255649d2cd..157135117fbee 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">bundleProduct::bundle_dynamic_product</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> @@ -15,6 +16,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="isRequired" xsi:type="string">No</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml index 7077f367795ed..cb32742a0ce6b 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/4" xsi:type="string">Bundle Product</data> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml index 3ad8cff31eaf8..bfbe233b9dc1b 100644 --- a/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation4" summary="Export bundle products" ticketId="MAGETWO-30602"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">bundleProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php index 4883d7819c288..b040397139451 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php @@ -6,7 +6,7 @@ namespace Magento\Captcha\Test\Constraint; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\Constraint\AbstractConstraint; /** diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml index 060fc5f346fda..742eabb61f371 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> - <page name="ContactIndex" mca="contact/index/index" module="Magento_Contact"> + <page name="ContactIndexCaptcha" mca="contact/index/index" module="Magento_Captcha"> <block name="contactUs" class="Magento\Captcha\Test\Block\Form\ContactUs" locator="#contact-form" strategy="css selector" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml index 12b9808adb9e0..0c0e06d63b6c9 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml @@ -14,6 +14,7 @@ <data name="attempts" xsi:type="number">3</data> <data name="captcha" xsi:type="string">111</data> <data name="configData" xsi:type="string">captcha_storefront_user_edit_failures_number, customer_max_login_failures_number</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Security\Test\Constraint\AssertCustomerIsLocked" /> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerIsLockedOnBackend" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml index 186439bb9f157..9242bfbef2374 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml @@ -12,6 +12,7 @@ <data name="customAdmin/data/captcha" xsi:type="string">111</data> <data name="pageTitle" xsi:type="string">Dashboard</data> <data name="configData" xsi:type="string">captcha_backend_login</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php index d8c9bf1f719de..0de71c3a416c8 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php @@ -8,7 +8,7 @@ use Magento\Captcha\Test\Constraint\AssertCaptchaFieldOnContactUsForm; use Magento\Contact\Test\Fixture\Comment; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml index a88cd98e3c31b..1a25afeabc5de 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml @@ -12,6 +12,7 @@ <data name="comment/data/captcha" xsi:type="string">111</data> <data name="comment/data/customer/dataset" xsi:type="string">default</data> <data name="configData" xsi:type="string">captcha_storefront_contact_us</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Contact\Test\Constraint\AssertContactUsSuccessMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml index 8e4327db5eddc..8068b2cbc050e 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Captcha\Test\TestCase\CaptchaOnStoreFrontLoginTest" summary="Check CAPTCHA on StoreFront Login Page" ticketId="MAGETWO-43639"> <variation name="CaptchaOnStoreFrontLoginTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="captcha" xsi:type="string">111</data> <data name="configData" xsi:type="string">captcha_storefront_login</data> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml index 8e83c189efc2f..b0ce6dfa561ae 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Captcha\Test\TestCase\CaptchaOnStoreFrontRegisterTest" summary="Check CAPTCHA on StoreFront Register Page" ticketId="MAGETWO-43602"> <variation name="CaptchaOnStoreFrontRegisterTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">register_customer</data> <data name="customer/data/captcha" xsi:type="string">111</data> <data name="configData" xsi:type="string">captcha_storefront_register</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php index 2035e7e83200f..30a323eebb736 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php @@ -187,6 +187,8 @@ public function assignCategory($parentCategoryName, $childCategoryName) */ public function expandAllCategories() { + $this->getTemplateBlock()->waitLoader(); $this->_rootElement->find($this->expandAll)->click(); + $this->getTemplateBlock()->waitLoader(); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php index 2ac4fb81ae604..d591f3b44462a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php @@ -403,4 +403,21 @@ public function getFileOptionElements() { return $this->_rootElement->getElements($this->hintMessage); } + + /** + * @inheritdoc + */ + protected function _fill(array $fields, SimpleElement $element = null) + { + $context = ($element === null) ? $this->_rootElement : $element; + foreach ($fields as $name => $field) { + $element = $this->getElement($context, $field); + if (!$element->isDisabled()) { + $element->getContext()->hover(); + $element->setValue($field['value']); + } else { + throw new \Exception("Unable to set value to field '$name' as it's disabled."); + } + } + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php index 090042140a548..48769126d88ea 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php @@ -48,7 +48,7 @@ public function getSelectSortType() public function getSortType() { $content = $this->_rootElement->find($this->sorter)->getText(); - return explode("\n", $content); + return array_values(array_filter(array_map('trim', explode("\n", $content)))); } /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php index dcc8cce970098..4e8e0f97d70d5 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php @@ -128,7 +128,8 @@ class CustomOptions extends Form * * @var string */ - private $validationErrorMessage = '//div[@class="mage-error"][contains(text(), "required field")]'; + private $validationErrorMessage = '//div[@class="mage-error"][contains(text(), "required field")' . + 'and not(contains(@style,\'display\'))]'; /** * Get product options @@ -148,6 +149,7 @@ public function getOptions(FixtureInterface $product) foreach ($dataOptions as $option) { $title = $option['title']; if (!isset($listCustomOptions[$title])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Can't find option: \"{$title}\""); } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php index 7ca5bfd2be140..a34b97b4ce228 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php @@ -10,7 +10,6 @@ use Magento\Mtf\Client\Locator; /** - * Class Search * Block for "Search" section */ class Search extends Block @@ -77,6 +76,7 @@ public function search($keyword, $length = null) $keyword = substr($keyword, 0, $length); } $this->fillSearch($keyword); + $this->waitForElementEnabled($this->searchButton); $this->_rootElement->find($this->searchButton)->click(); } @@ -157,4 +157,24 @@ public function clickSuggestedText($text) $searchAutocomplete = sprintf($this->searchAutocomplete, $text); $this->_rootElement->find($searchAutocomplete, Locator::SELECTOR_XPATH)->click(); } + + /** + * Wait for element is enabled. + * + * @param string $selector + * @param string $strategy + * @return bool|null + */ + public function waitForElementEnabled($selector, $strategy = Locator::SELECTOR_CSS) + { + $browser = $this->browser; + + return $browser->waitUntil( + function () use ($browser, $selector, $strategy) { + $element = $browser->find($selector, $strategy); + + return !$element->isDisabled() ? true : null; + } + ); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml index d529f74865985..014d685cfdb7c 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml @@ -179,5 +179,31 @@ <item name="dataset" xsi:type="string">catalogProductSimple::default</item> </field> </dataset> + <dataset name="default_subcategory_with_anchored_parent_with_product"> + <field name="name" xsi:type="string">DefaultSubcategory%isolation%</field> + <field name="url_key" xsi:type="string">default-subcategory-%isolation%</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_anchored_category_with_product</item> + </field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">Yes</field> + <field name="category_products" xsi:type="array"> + <item name="dataset" xsi:type="string">catalogProductSimple::default</item> + </field> + </dataset> + <dataset name="default_anchored_category_with_product"> + <field name="name" xsi:type="string">Category%isolation%</field> + <field name="url_key" xsi:type="string">category%isolation%</field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">Yes</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_category</item> + </field> + <field name="category_products" xsi:type="array"> + <item name="dataset" xsi:type="string">catalogProductSimple::product_5_dollar</item> + </field> + </dataset> </repository> </config> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml index 96a9d91a8e5f3..e92edf4a143b9 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\AdvancedMoveCategoryEntityTest" summary="Move category from one to another" ticketId="MAGETWO-27319"> <variation name="AdvancedMoveCategoryEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="childCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="parentCategory/dataset" xsi:type="string">default</data> <data name="moveLevel" xsi:type="number">1</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml index c6a66beac7c79..69093b8adb8db 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml @@ -17,7 +17,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForm" /> </variation> <variation name="CreateCategoryEntityTestVariation2_RootCategory_AllFields"> - <data name="issue" xsi:type="string">MAGETWO-60635: [CE][Categories] Design update dates are incorrect after save</data> <data name="description" xsi:type="string">Create root category with all fields</data> <data name="addCategory" xsi:type="string">addRootCategory</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -58,7 +57,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="CreateCategoryEntityTestVariation4_Subcategory_AllFields"> - <data name="issue" xsi:type="string">MAGETWO-60635: [CE][Categories] Design update dates are incorrect after save</data> <data name="description" xsi:type="string">Create not anchor subcategory specifying all fields</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> @@ -96,6 +94,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForAssignedProducts" /> </variation> <variation name="CreateCategoryEntityTestVariation5_Anchor_MostOfFields"> + <data name="tag" xsi:type="string">test_type:acceptance_test</data> <data name="description" xsi:type="string">Create anchor subcategory with all fields</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> @@ -145,7 +144,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="CreateCategoryEntityTestVariation8_WithProducts" summary="Assign Products at the Category Level" ticketId="MAGETWO-16351"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/is_active" xsi:type="string">Yes</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml index 6951194308bc9..77ed04d40b77a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\DeleteCategoryEntityTest" summary="Delete Category" ticketId="MAGETWO-23303"> <variation name="DeleteCategoryEntityTestVariation1_RootCategory" summary="Can delete a root category not assigned to any store"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/dataset" xsi:type="string">root_category</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategorySuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryAbsenceOnBackend" /> </variation> <variation name="DeleteCategoryEntityTestVariation2_Subcategory" summary="Can delete a subcategory"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/dataset" xsi:type="string">root_subcategory</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategorySuccessDeleteMessage" /> <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryNotInGrid" /> @@ -20,6 +22,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryAbsenceOnFrontend" /> </variation> <variation name="DeleteCategoryEntityTestVariation3_RootCategory_AssignedToStore" summary="Cannot delete root category assigned to some store"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/dataset" xsi:type="string">default_category</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryCannotBeDeleted" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml index 446011902c096..b4fd843ca800f 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\MoveCategoryEntityTest" summary="Move category from one to another" ticketId="MAGETWO-27319"> <variation name="MoveCategoryEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="childCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="parentCategory/dataset" xsi:type="string">default</data> <data name="nestingLevel" xsi:type="string">3</data> @@ -15,8 +16,8 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryInGrid" /> </variation> <variation name="MoveCategoryEntityTestVariation2" summary="Move default subcategory with anchored parent to default subcategory" ticketId="MAGETWO-21202"> - <data name="issue" xsi:type="string">MAGETWO-65147: Category is not present in Layered navigation block when anchor is on</data> - <data name="childCategory/dataset" xsi:type="string">default_subcategory_with_anchored_parent</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="childCategory/dataset" xsi:type="string">default_subcategory_with_anchored_parent_with_product</data> <data name="parentCategory/dataset" xsi:type="string">default</data> <data name="moveLevel" xsi:type="number">2</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryMovedMessage" /> @@ -25,9 +26,9 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryBreadcrumbs" /> </variation> <variation name="MoveCategoryEntityTestVariation3" summary="Move default anchored subcategory with anchored parent to default subcategory" ticketId="MAGETWO-21202"> - <data name="issue" xsi:type="string">MAGETWO-65147: Category is not present in Layered navigation block when anchor is on</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="childCategory/dataset" xsi:type="string">default_subcategory_with_anchored_parent</data> - <data name="childCategory/data/parent_id/dataset" xsi:type="string">default_anchor_subcategory_with_anchored_parent</data> + <data name="childCategory/data/parent_id/dataset" xsi:type="string">default_subcategory_with_anchored_parent_with_product</data> <data name="parentCategory/dataset" xsi:type="string">default_category</data> <data name="moveLevel" xsi:type="number">2</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryMovedMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml index 94d99dd6b7b24..53a7debffa438 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\MyTest" summary="Test child categories should not include in menu" ticketId="MAGETWO-72238"> <variation name="CategoryIncludeInNavigationMenuAndSubcategoryNotIncludeInNavigationMenu" summary="Active category and check that category is visible on navigation menu and subcategory is not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -16,6 +17,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertSubCategoryNotInNavigationMenu" /> </variation> <variation name="CategoryAndSubcategotyNotIncludeInNavigationMenu1" summary="Turn off include_in_menu category and check that category and subcategory are not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -24,6 +26,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertSubCategoryNotInNavigationMenu" /> </variation> <variation name="InactiveCategoryAndSubcategotyNotIncludeInNavigationMenu" summary="Inactive category and check that category and subcategory are not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">No</data> @@ -32,6 +35,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertSubCategoryNotInNavigationMenu" /> </variation> <variation name="CategoryAndSubcategotyNotIncludeInNavigationMenu2" summary="Turn off include_in_menu category, inactive category and check that category and subcategory are not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">No</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml index 5f14f579a4271..99f4b6718feb9 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateCategoryEntityFlatDataTest" summary="Update Category if Use Category Flat (Cron is ON, 'Update on Save' Mode)" ticketId="MAGETWO-20169"> <variation name="UpdateCategoryEntityFlatDataTestVariation1" summary="Update Category with custom name and description"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/name" xsi:type="string">Name%isolation%</data> <data name="category/data/description" xsi:type="string">Category Description Updated</data> @@ -23,6 +24,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="UpdateCategoryEntityFlatDataTestVariation2" summary="Include category to navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="initialCategory/data/include_in_menu" xsi:type="string">No</data> <data name="category/data/include_in_menu" xsi:type="string">Yes</data> @@ -37,6 +39,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryInNavigationMenu" /> </variation> <variation name="UpdateCategoryEntityFlatDataTestVariation3" summary="Update category and assert assigned products"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/category_products/dataset" xsi:type="string">catalogProductSimple::default</data> <data name="indexers/0" xsi:type="string">Category Flat Data</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml index 76d5a532271ef..1cda62997e189 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateCategoryEntityTest" summary="Update Category" ticketId="MAGETWO-23290"> <variation name="UpdateCategoryEntityTestVariation1_Name_Description_UrlKey_MetaTitle_ExcludeFromMenu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/name" xsi:type="string">Name%isolation%</data> <data name="category/data/include_in_menu" xsi:type="string">No</data> @@ -22,6 +23,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="UpdateCategoryEntityTestVariation2_SortProductsBy_DefaultProductSorting_AddProduct"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/available_product_listing_config" xsi:type="string">Yes</data> <data name="category/data/default_product_listing_config" xsi:type="string">No</data> @@ -36,6 +38,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForAssignedProducts" /> </variation> <variation name="UpdateCategoryEntityTestVariation3_MakeInactive"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/is_active" xsi:type="string">No</data> <data name="category/data/name" xsi:type="string">Name%isolation%</data> @@ -44,6 +47,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryIsNotActive" /> </variation> <variation name="UpdateCategoryEntityTestVariation4_ChangeCategoryNameOnStoreView" summary="Update Category with custom Store View."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/store_id/dataset" xsi:type="string">custom</data> <data name="category/data/use_default_name" xsi:type="string">No</data> <data name="category/data/name" xsi:type="string">Category %isolation%</data> @@ -51,6 +55,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryWithCustomStoreOnFrontend" /> </variation> <variation name="UpdateCategoryEntityTestVariation5_ChangeCategoryUrlOnStoreView" summary="Update URL Key with custom Store View." ticketId="MAGETWO-16471"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/store_id/dataset" xsi:type="string">custom</data> <data name="category/data/use_default_url_key" xsi:type="string">No</data> @@ -59,6 +64,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryOnCustomStore" /> </variation> <variation name="UpdateCategoryEntityTestVariation6_CheckCategoryDefaultUrlOnStoreView" summary="Check default URL Key on the custom Store View." ticketId="MAGETWO-64337"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default_with_custom_url</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/store_id/dataset" xsi:type="string">custom</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml index fd2c9afaea160..0c7d88cc920b2 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateInactiveCategoryEntityFlatDataTest" summary="Update Category if Use Category Flat (Cron is ON, 'Update on Save' Mode)" ticketId="MAGETWO-20169"> <variation name="UpdateInactiveCategoryEntityFlatDataTestVariation1" summary="Inactive category and check that category is absent on frontend"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/is_active" xsi:type="string">No</data> <data name="indexers/0" xsi:type="string">Category Flat Data</data> @@ -22,6 +23,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryAbsenceOnFrontend" /> </variation> <variation name="UpdateInactiveCategoryEntityFlatDataTestVariation2" summary="Inactive category and check that category is not active on frontend"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="initialCategory/data/is_active" xsi:type="string">No</data> <data name="category/data/is_active" xsi:type="string">No</data> @@ -36,6 +38,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryIsNotActive" /> </variation> <variation name="UpdateInactiveCategoryEntityFlatDataTestVariation3" summary="Exclude category from navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/include_in_menu" xsi:type="string">No</data> <data name="indexers/0" xsi:type="string">Category Flat Data</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml index 9126b619bbfdb..e8a5fd355da7d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateTopCategoryEntityTest" summary="Update top category url" ticketId="MAGETWO-27327"> <variation name="UpdateCategoryEntityTestVariation1" summary="Update top category url and do not create redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="nestingLevel" xsi:type="number">3</data> <data name="category/data/url_key" xsi:type="string">cat1-rewrite%isolation%</data> @@ -17,6 +18,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewritesCategoriesInGrid" /> </variation> <variation name="UpdateCategoryEntityTestVariation2" summary="Update top category url and create redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="nestingLevel" xsi:type="number">3</data> <data name="category/data/url_key" xsi:type="string">cat1-rewrite%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml index 260095048431e..08bff9f70708a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Catalog > Products</data> <data name="pageTitle" xsi:type="string">Products</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Catalog > Categories</data> <data name="pageTitle" xsi:type="string">Default Category (ID: 2)</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Product</data> <data name="pageTitle" xsi:type="string">Product Attributes</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest12"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Attribute Set</data> <data name="pageTitle" xsi:type="string">Attribute Sets</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddToCartCrossSellTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddToCartCrossSellTest.xml index 874fc3f670362..b1f093162fa4b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddToCartCrossSellTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddToCartCrossSellTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\AddToCartCrossSellTest" summary="Promote Products as Cross-Sells" ticketId="MAGETWO-12390"> <variation name="AddToCartCrossSellTestVariation1" method="test"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="products" xsi:type="string">simple1::catalogProductSimple::product_with_category,simple2::catalogProductSimple::product_with_category,config1::configurableProduct::two_options_with_fixed_price</data> <data name="promotedProducts" xsi:type="string">simple1:simple2,config1;config1:simple2</data> <data name="navigateProductsOrder" xsi:type="string">simple1,config1,simple2</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.php deleted file mode 100644 index cb5ad93ee429b..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.php +++ /dev/null @@ -1,146 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Catalog\Test\TestCase\Product; - -use Magento\Config\Test\TestStep\SetupConfigurationStep; -use Magento\Catalog\Test\Page\Category\CatalogCategoryView; -use Magento\Mtf\Fixture\FixtureFactory; -use Magento\Mtf\TestCase\Injectable; -use Magento\Catalog\Test\Fixture\Category; - -/** - * Preconditions: - * 1. Use Flat Catalog Category & Use Flat Catalog Product are enabled (Store/Configuration/Catalog/Catalog/Storefront) - * - * Steps: - * 1. Assign 16 or more products to the same category (e.g. 20 products) - * 2. Go to Storefront and navigate to this category - * 3. Click on Page 2 or any further page - * 4. Go back to page 1 and change № of products per page from 9 to any number (e.g 12) - * 5. Click on Page 2 or any further page - * 5. Perform assertions. - * - * @ZephyrId MAGETWO-67570 - */ -class CreateFlatCatalogProduct extends Injectable -{ - /* tags */ - const MVP = 'yes'; - /* end tags */ - - /** - * Configuration data - * - * @var string - */ - private $configData; - - /** - * Factory for Fixtures - * - * @var FixtureFactory - */ - private $fixtureFactory; - - /** - * Category fixture - * - * @var Category - */ - private $category; - - /** - * CatalogCategoryView page - * - * @var CatalogCategoryView - */ - private $catalogCategoryView; - - /** - * Prepare data - * - * @param Category $category - * @return array - */ - public function __prepare(Category $category) - { - $category->persist(); - return [ - 'category' => $category - ]; - } - - /** - * Injection data - * - * @param Category $category - * @param FixtureFactory $fixtureFactory - * @param CatalogCategoryView $catalogCategoryView - * @return void - */ - public function __inject( - Category $category, - FixtureFactory $fixtureFactory, - CatalogCategoryView $catalogCategoryView - ) { - $this->category = $category; - $this->fixtureFactory = $fixtureFactory; - $this->catalogCategoryView = $catalogCategoryView; - } - - /** - * Run create flat catalog product - * - * @param string $configData - * @param string $productsCount - * @return array - */ - public function test($configData, $productsCount) - { - $this->objectManager->create(SetupConfigurationStep::class, ['configData' => $this->configData])->run(); - $this->createBulkOfProducts($productsCount); - $this->configData = $configData; - return ['category' => $this->category, 'catalogCategoryView' => $this->catalogCategoryView]; - } - - /** - * Clear data after test - * - * @return void - */ - public function tearDown() - { - $this->objectManager->create( - SetupConfigurationStep::class, - ['configData' => $this->configData, 'rollback' => true] - )->run(); - } - - /** - * Create products for tests - * - * @param $productsCount - * @return void - */ - private function createBulkOfProducts($productsCount) - { - for ($counter = 1; $counter <= $productsCount; $counter++) { - $product = $this->fixtureFactory->createByCode( - 'catalogProductSimple', - [ - 'dataset' => 'default', - 'data' => [ - 'category_ids' => [ - 'category' => $this->category - ] - ] - ] - ); - $product->persist(); - } - } -} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.xml deleted file mode 100644 index 17d362f35ec57..0000000000000 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?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="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Catalog\Test\TestCase\Product\CreateFlatCatalogProduct" summary="Create flat catalog Product" ticketId="MAGETWO-67570"> - <variation name="CheckPaginationInStorefront" ticketId="MAGETWO-67570"> - <data name="configData" xsi:type="string">category_flat,product_flat</data> - <data name="productsCount" xsi:type="number">19</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertPaginationCorrectOnStoreFront" /> - </variation> - </testCase> -</config> \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.php new file mode 100644 index 0000000000000..8f11f31a6dff7 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\TestCase\Product; + +use Magento\Config\Test\TestStep\SetupConfigurationStep; +use Magento\Catalog\Test\Page\Category\CatalogCategoryView; +use Magento\Mtf\Fixture\FixtureFactory; +use Magento\Mtf\TestCase\Injectable; +use Magento\Catalog\Test\Fixture\Category; + +/** + * Preconditions: + * 1. Use Flat Catalog Category & Use Flat Catalog Product are enabled (Store/Configuration/Catalog/Catalog/Storefront) + * + * Steps: + * 1. Assign 16 or more products to the same category (e.g. 20 products) + * 2. Go to Storefront and navigate to this category + * 3. Click on Page 2 or any further page + * 4. Go back to page 1 and change № of products per page from 9 to any number (e.g 12) + * 5. Click on Page 2 or any further page + * 5. Perform assertions. + * + * @ZephyrId MAGETWO-67570 + */ +class CreateFlatCatalogProductTest extends Injectable +{ + /* tags */ + const MVP = 'yes'; + /* end tags */ + + /** + * Configuration data + * + * @var string + */ + private $configData; + + /** + * Factory for Fixtures + * + * @var FixtureFactory + */ + private $fixtureFactory; + + /** + * Category fixture + * + * @var Category + */ + private $category; + + /** + * CatalogCategoryView page + * + * @var CatalogCategoryView + */ + private $catalogCategoryView; + + /** + * Prepare data + * + * @param Category $category + * @return array + */ + public function __prepare(Category $category) + { + $category->persist(); + return [ + 'category' => $category + ]; + } + + /** + * Injection data + * + * @param Category $category + * @param FixtureFactory $fixtureFactory + * @param CatalogCategoryView $catalogCategoryView + * @return void + */ + public function __inject( + Category $category, + FixtureFactory $fixtureFactory, + CatalogCategoryView $catalogCategoryView + ) { + $this->category = $category; + $this->fixtureFactory = $fixtureFactory; + $this->catalogCategoryView = $catalogCategoryView; + } + + /** + * Run create flat catalog product + * + * @param string $configData + * @param string $productsCount + * @return array + */ + public function test($configData, $productsCount) + { + $this->objectManager->create(SetupConfigurationStep::class, ['configData' => $this->configData])->run(); + $this->createBulkOfProducts($productsCount); + $this->configData = $configData; + return ['category' => $this->category, 'catalogCategoryView' => $this->catalogCategoryView]; + } + + /** + * Clear data after test + * + * @return void + */ + public function tearDown() + { + $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => $this->configData, 'rollback' => true] + )->run(); + } + + /** + * Create products for tests + * + * @param $productsCount + * @return void + */ + private function createBulkOfProducts($productsCount) + { + for ($counter = 1; $counter <= $productsCount; $counter++) { + $product = $this->fixtureFactory->createByCode( + 'catalogProductSimple', + [ + 'dataset' => 'default', + 'data' => [ + 'category_ids' => [ + 'category' => $this->category + ] + ] + ] + ); + $product->persist(); + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.xml new file mode 100644 index 0000000000000..45161e1471f66 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.xml @@ -0,0 +1,17 @@ +<?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="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> + <testCase name="Magento\Catalog\Test\TestCase\Product\CreateFlatCatalogProductTest" summary="Create flat catalog Product" ticketId="MAGETWO-67570"> + <variation name="CheckPaginationInStorefront" ticketId="MAGETWO-67570"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="configData" xsi:type="string">category_flat,product_flat</data> + <data name="productsCount" xsi:type="number">19</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertPaginationCorrectOnStoreFront" /> + </variation> + </testCase> +</config> \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml index aecdbc5362fbb..bdea332a3af0d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\CreateSimpleProductEntityByAttributeMaskSkuTest" summary="Create Simple Product with attribute sku mask" ticketId="MAGETWO-59861"> <variation name="CreateSimpleProductEntityByAttributeMaskSkuTest1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">attribute_product_mask_sku</data> <data name="description" xsi:type="string">Create product with country of manufacture attribute sku mask</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml index 4a3d80b0b6090..f97735304baa5 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml @@ -25,6 +25,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInventoryMaxAllowedQty" /> </variation> <variation name="CreateSimpleProductEntityTestVariation13"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-48850: Filtering Category Products using scope selector</data> <data name="description" xsi:type="string">Create simple product and check search by sku</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> @@ -42,6 +44,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="CreateSimpleProductEntityTestVariation14"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MC-6220: Add products to wishlist from Category page with multiple wishlist enabled</data> <data name="description" xsi:type="string">Create simple product and check visibility in category</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml index a40c715bb3ac3..d6b75e5c3a6e4 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml @@ -8,6 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\CreateSimpleProductEntityPartTwoTest" summary="Create Simple Product" ticketId="MAGETWO-23414"> <variation name="CreateSimpleProductEntityTestVariation23" summary="Create Simple Product with Creating New Category (Required Fields Only)" ticketId="MAGETWO-27293"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MC-234: Admin should be able to create category from the product page</data> <data name="product/data/category_ids/new_category" xsi:type="string">yes</data> <data name="product/data/category_ids/dataset" xsi:type="string">default_subcategory_without_url_key</data> <data name="product/data/website_ids/0/dataset" xsi:type="string">default</data> @@ -23,6 +25,8 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteProductInGrid" /> </variation> <variation name="CreateSimpleProductEntityTestVariation24" summary="Create Simple Product and Assigning It to Category" ticketId="MAGETWO-12514"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-23414: Create Simple Product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> @@ -79,6 +83,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> </variation> <variation name="CreateSimpleProductEntityTestVariation28" summary="Create product with tier price for not logged in customer"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-68921: Apply Tier Price to a product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> @@ -118,6 +124,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCart" /> </variation> <variation name="CreateSimpleProductEntityWithTierPriceTestVariation1" summary="Create Simple Product with fixed tier price."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-68921: Apply Tier Price to a product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> @@ -127,6 +135,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductTierPriceInCart" /> </variation> <variation name="CreateSimpleProductEntityWithTierPriceTestVariation2" summary="Create Simple Product with percentage tier price."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-68921: Apply Tier Price to a product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php index 7a677dbea983c..4d866f716d70c 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php @@ -79,7 +79,6 @@ public function testCreate( $flushCache = false, $configData = null ) { - $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/1620'); $this->configData = $configData; $this->flushCache = $flushCache; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index 8732157e5c657..840dc0b0812b2 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -182,6 +182,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="CreateSimpleProductEntityTestVariation10" summary="Create in stock product and check threshold" ticketId="MAGETWO-43345"> + <data name="issue" xsi:type="string">https://github.com/magento-engcom/msi/issues/1620</data> + <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="configData" xsi:type="string">inventory_threshold_5</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml index 1449e7df0ce61..a9c78117d7b69 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\CreateVirtualProductEntityTest" summary="Create Virtual Product" ticketId="MAGETWO-23417"> <variation name="CreateVirtualProductEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product with required fields</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -17,7 +18,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="CreateVirtualProductEntityTestVariation2" summary="Create product with tier price"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> @@ -51,6 +52,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="CreateVirtualProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product with tier price for "General" group</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -71,6 +73,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductTierPriceOnProductPageWithCustomer" /> </variation> <variation name="CreateVirtualProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product with custom options suite and import options</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -86,6 +89,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> </variation> <variation name="CreateVirtualProductEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product without manage stock</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -102,6 +106,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInStock" /> </variation> <variation name="CreateVirtualProductEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product out of stock with tier price</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml index fc566e855c0ff..2d91f4a7024e5 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml @@ -10,12 +10,13 @@ <variation name="DeleteProductEntityTestVariation1"> <data name="products" xsi:type="string">catalogProductSimple::default</data> <data name="isRequired" xsi:type="string">Yes</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> + <data name="tag" xsi:type="string">to_maintain:yes, mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductNotInGrid" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">catalogProductVirtual::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> @@ -23,6 +24,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">catalogProductSimple::with_one_custom_option</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ManageProductsStockTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ManageProductsStockTest.xml index 33b578672d48b..d005c05a9cba2 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ManageProductsStockTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ManageProductsStockTest.xml @@ -15,6 +15,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductQtyInShoppingCart" /> </variation> <variation name="ManageProductsStockTestVariation2" summary="Checking that Out of Stock products are not visible in category" ticketId="MAGETWO-13645"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="product/data/stock_data/manage_stock" xsi:type="string">Yes</data> <data name="product/data/quantity_and_stock_status/qty" xsi:type="string">5</data> @@ -33,6 +34,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> </variation> <variation name="ManageProductsStockTestVariation3" summary="Add In Stock product to cart" ticketId="MAGETWO-13645"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="product/data/stock_data/manage_stock" xsi:type="string">Yes</data> <data name="product/data/quantity_and_stock_status/qty" xsi:type="string">5</data> @@ -51,6 +53,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="ManageProductsStockTestVariation4" summary="Enable displaying of out of stock products in category" ticketId="MAGETWO-13645"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="product/data/stock_data/manage_stock" xsi:type="string">Yes</data> <data name="product/data/quantity_and_stock_status/qty" xsi:type="string">5</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml index fa82fd90268fd..6936315a12818 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\MassProductUpdateStatusTest" summary="Update status of Products Using Mass Actions" ticketId="MAGETWO-60847"> <variation name="MassProductStatusUpdateTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProducts" xsi:type="array"> <item name ="0" xsi:type="string">catalogProductSimple::simple_10_dollar</item> <item name ="1" xsi:type="string">catalogProductSimple::simple_10_dollar</item> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml index 6f3803d832c6d..d2fe51ecd810d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\MassProductUpdateTest" summary="Edit Products Using Mass Actions" ticketId="MAGETWO-21128"> <variation name="MassProductPriceUpdateTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">product_flat</data> <data name="initialProducts/0" xsi:type="string">catalogProductSimple::simple_10_dollar</data> <data name="initialProducts/1" xsi:type="string">catalogProductSimple::simple_10_dollar</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml index cf52597cfc52f..0db197ba3b385 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\NavigateUpSellProductsTest" summary="Promote Products as Up-Sells" ticketId="MAGETWO-12391"> <variation name="NavigateUpSellProductsTestVariation1" method="test"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no, mftf_migrated:yes</data> <data name="products" xsi:type="string">simple1::catalogProductSimple::product_with_category,simple2::catalogProductSimple::product_with_category,config1::configurableProduct::two_options_with_fixed_price</data> <data name="promotedProducts" xsi:type="string">simple1:simple2,config1;config1:simple2</data> <data name="navigateProductsOrder" xsi:type="string">simple1,config1,simple2</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml index 8fbb64f4579b1..a563d369f95cc 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ProductTypeSwitchingOnCreationTest" summary="Product Type Switching on Creation" ticketId="MAGETWO-29398"> <variation name="ProductTypeSwitchingOnCreationTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> + <data name="tag" xsi:type="string">stable:no, mftf_migrated:yes</data> <data name="createProduct" xsi:type="string">simple</data> <data name="product" xsi:type="string">configurableProduct::default</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> @@ -23,16 +23,19 @@ <variation name="ProductTypeSwitchingOnCreationTestVariation2"> <data name="createProduct" xsi:type="string">simple</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnCreationTestVariation3"> <data name="createProduct" xsi:type="string">configurable</data> <data name="product" xsi:type="string">catalogProductSimple::product_without_category</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnCreationTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="issue" xsi:type="string">MSI-1624</data> <data name="createProduct" xsi:type="string">configurable</data> <data name="product" xsi:type="string">catalogProductVirtual::required_fields</data> @@ -42,11 +45,12 @@ <variation name="ProductTypeSwitchingOnCreationTestVariation5"> <data name="createProduct" xsi:type="string">virtual</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnCreationTestVariation6"> - <data name="tag" xsi:type="string">stable:no</data> + <data name="tag" xsi:type="string">stable:no, mftf_migrated:yes</data> <data name="createProduct" xsi:type="string">virtual</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> @@ -71,6 +75,7 @@ <variation name="ProductTypeSwitchingOnCreationTestVariation8"> <data name="createProduct" xsi:type="string">downloadable</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php index 43741393e7968..90cd6bdb76328 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php @@ -143,5 +143,6 @@ protected function clearDownloadableData() /** @var Downloadable $downloadableInfoTab */ $downloadableInfoTab = $this->catalogProductEdit->getProductForm()->getSection('downloadable_information'); $downloadableInfoTab->getDownloadableBlock('Links')->clearDownloadableData(); + $downloadableInfoTab->setIsDownloadable('No'); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml index f3df374a8bac8..5fa1cfe5e5911 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml @@ -11,7 +11,6 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">configurableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -21,7 +20,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> <data name="actionName" xsi:type="string">-</data> @@ -29,7 +27,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation3"> - <data name="tag" xsi:type="string">stable:no</data> <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::product_without_category</data> <data name="actionName" xsi:type="string">deleteVariations</data> @@ -40,12 +37,10 @@ <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::required_fields</data> <data name="actionName" xsi:type="string">deleteVariations</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation5"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> @@ -56,7 +51,6 @@ <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -69,7 +63,6 @@ <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> @@ -81,15 +74,13 @@ <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation9"> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> - <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> + <data name="actionName" xsi:type="string">clearDownloadableData</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -99,7 +90,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation10"> - <data name="tag" xsi:type="string">stable:no</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> <data name="actionName" xsi:type="string">clearDownloadableData</data> @@ -110,7 +100,6 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml index 2a46abdc2fd15..ce99a61c33bac 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\UpdateSimpleProductEntityTest" summary="Update Simple Product" ticketId="MAGETWO-23544"> <variation name="UpdateSimpleProductEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Catalog, Search</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -24,6 +25,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Not Visible Individually</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -38,6 +40,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Catalog</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -55,6 +58,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Search</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -72,6 +76,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update stock to Out of Stock</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -89,6 +94,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update product status to offline</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -103,6 +109,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update category</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/category_ids/dataset" xsi:type="string">default</data> @@ -118,6 +125,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation8" summary="Edit Simple Product" ticketId="MAGETWO-12428"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/category_ids/dataset" xsi:type="string">default</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -128,13 +136,14 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation9" summary="Unassign Products from the Category" ticketId="MAGETWO-12417"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/category_ids/dataset" xsi:type="string"> -</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductNotVisibleInCategory" /> </variation> <variation name="EditSimpleProductTestVariation10" summary="Edit product with enabled flat" ticketId="MAGETWO-21125"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">product_flat</data> <data name="initialProduct/dataset" xsi:type="string">simple_10_dollar</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> @@ -146,6 +155,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCategory" /> </variation> <variation name="EditSimpleProductTestVariation11" summary="Update simple product with custom option"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> <data name="product/data/sku" xsi:type="string">test_simple_product_%isolation%</data> @@ -160,6 +170,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCart" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation12" summary="Verify data overriding on Store View level" ticketId="MAGETWO-50640"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="store/dataset" xsi:type="string">custom</data> <data name="product/data/use_default_name" xsi:type="string">No</data> @@ -168,7 +179,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductNameOnDifferentStoreViews" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation13" summary="Price overriding on Store View level" ticketId="MAGETWO-58861"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="configData" xsi:type="string">price_scope_website</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="store/dataset" xsi:type="string">custom</data> @@ -178,6 +189,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPriceOnDifferentStoreViews" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation14" summary="An error appears on open tier price with locale formatting" ticketId="MAGETWO-62076"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">simple_with_hight_tier_price</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductFormattingTierPrice" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml index daa19a8fe6fe8..e2715bb6b4b81 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\UpdateVirtualProductEntityTest" summary="Update Virtual Product" ticketId="MAGETWO-26204"> <variation name="UpdateVirtualProductEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -28,6 +29,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">120.00</data> @@ -47,6 +49,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">185.00</data> @@ -67,6 +70,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -82,6 +86,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">5.00</data> @@ -97,6 +102,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">145.00</data> @@ -116,6 +122,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -133,6 +140,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation8"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">5.00</data> @@ -149,6 +157,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">120.00</data> @@ -168,6 +177,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -183,6 +193,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ValidateOrderOfProductTypeTest.xml index 0d1a34ab52f97..ec776128fdfe3 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest" summary="Validate product types order on product grid page" ticketId="MAGETWO-37146"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/0" xsi:type="string">Simple Product</data> <data name="menu/3" xsi:type="string">Virtual Product</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductTypeOrderOnCreate" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateAttributeSetEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateAttributeSetEntityTest.xml index 86bacd925ba05..13e05b1d122cb 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateAttributeSetEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateAttributeSetEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\CreateAttributeSetEntityTest" summary="Create Attribute Set (Attribute Set)" ticketId="MAGETWO-25104"> <variation name="CreateAttributeSetEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/data/attribute_set_name" xsi:type="string">AttributeSet%isolation%</data> <data name="attributeSet/data/skeleton_set/dataset" xsi:type="string">default</data> <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeSetSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityFromProductPageTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityFromProductPageTest.xml index 73fbf556d099c..e15eab57cca01 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityFromProductPageTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityFromProductPageTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\CreateProductAttributeEntityFromProductPageTest" summary="Create Product Attribute from Product Page" ticketId="MAGETWO-30528"> <variation name="CreateProductAttributeEntityFromProductPageTestVariation1_Searchable_Global_Visible_Comparable_HtmlAllowed_UsedForSorting"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="attribute/data/frontend_input" xsi:type="string">Text Field</data> <data name="attribute/data/is_required" xsi:type="string">No</data> @@ -33,6 +34,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeIsUsedInSortOnFrontend" /> </variation> <variation name="CreateProductAttributeEntityFromProductPageTestVariation2_Filterable"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attribute/data/frontend_label" xsi:type="string">Dropdown_Admin_%isolation%</data> <data name="attribute/data/frontend_input" xsi:type="string">Dropdown</data> <data name="attribute/data/options/dataset" xsi:type="string">two_options</data> @@ -50,6 +52,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertProductAttributeIsConfigurable" /> </variation> <variation name="CreateProductAttributeEntityFromProductPageTestVariation3_Required"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="attribute/data/frontend_input" xsi:type="string">Text Field</data> <data name="attribute/data/is_required" xsi:type="string">Yes</data> @@ -59,6 +62,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeIsRequired" /> </variation> <variation name="CreateProductAttributeEntityFromProductPageTestVariation4_Unique"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="attribute/data/frontend_input" xsi:type="string">Text Field</data> <data name="attribute/data/is_required" xsi:type="string">No</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml index aae9ec6039f56..49725d08b63e8 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\CreateProductAttributeEntityTest" summary="Create Product Attribute" ticketId="MAGETWO-24767"> <variation name="CreateProductAttributeEntityTestVariation1"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Text Field</data> @@ -20,6 +19,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation2" summary="Create custom text attribute product field" ticketId="MAGETWO-17475"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Text Area</data> @@ -59,7 +59,6 @@ <data name="productAttribute/data/used_in_product_listing" xsi:type="string">Yes</data> <data name="productAttribute/data/is_used_for_promo_rules" xsi:type="string">Yes</data> <data name="productAttribute/data/used_for_sort_by" xsi:type="string">Yes</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeInGrid" /> <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeForm" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductByAttribute" /> @@ -68,7 +67,6 @@ <constraint name="Magento\CatalogRule\Test\Constraint\AssertProductAttributeIsUsedPromoRules" /> </variation> <variation name="CreateProductAttributeEntityTestVariation4"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Yes/No_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Yes/No</data> @@ -85,6 +83,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation5" summary="Create custom multiple select attribute product field" ticketId="MAGETWO-14862"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Multiple_Select_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Multiple Select</data> @@ -101,7 +100,6 @@ <data name="productAttribute/data/is_html_allowed_on_front" xsi:type="string">Yes</data> <data name="productAttribute/data/is_visible_on_front" xsi:type="string">Yes</data> <data name="productAttribute/data/used_in_product_listing" xsi:type="string">Yes</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeInGrid" /> <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeForm" /> <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> @@ -115,7 +113,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeOptionsOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation6" summary="Create custom dropdown attribute product field" ticketId="MAGETWO-17475, MAGETWO-14862"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Dropdown_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Dropdown</data> @@ -154,6 +152,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeOptionsOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation7" summary="Create custom price attribute product field" ticketId="MAGETWO-17475"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Price_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Price</data> @@ -176,7 +175,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeIsFilterableInSearch" /> </variation> <variation name="CreateProductAttributeEntityTestVariation8"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Fixed_Product_Tax_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Fixed Product Tax</data> @@ -193,7 +191,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation9"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Text Field</data> @@ -209,6 +206,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeIsUnique" /> </variation> <variation name="CreateProductAttributeEntityTestVariation10" summary="Create custom dropdown attribute with single quotation in option"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Dropdown_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Dropdown</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml index b12f4f1ad7d94..d674535b54abb 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteAssignedToTemplateProductAttributeTest" summary="Delete Assigned to Template Product Attribute" ticketId="MAGETWO-26011"> <variation name="DeleteAssignedToTemplateProductAttributeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="attributeSet/data/assigned_attributes/dataset" xsi:type="string">attribute_type_dropdown</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeSuccessDeleteMessage" /> @@ -16,6 +17,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeAbsenceInUnassignedAttributes" /> </variation> <variation name="DeleteAssignedToTemplateProductAttributeTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">default</data> <data name="attributeSet/data/assigned_attributes/dataset" xsi:type="string">attribute_type_text_field</data> <data name="assertProduct/data/name" xsi:type="string">Product name</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml index 361f2acb1d8a6..a83fce14d9381 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteAttributeSetTest" summary="Delete Attribute Set (Attribute Set)" ticketId="MAGETWO-25473"> <variation name="DeleteAttributeSetTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> + <data name="tag" xsi:type="string">stable:no, mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="attributeSet/data/assigned_attributes/dataset" xsi:type="string">default</data> <data name="product/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml index 6cca4b3f3685b..0dd47942c8fe0 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteProductAttributeEntityTest" summary="Delete Product Attribute" ticketId="MAGETWO-24998"> <variation name="DeleteProductAttributeEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attribute/dataset" xsi:type="string">attribute_type_text_field</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeSuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeAbsenceInGrid" /> @@ -15,7 +16,6 @@ <constraint name="Magento\ImportExport\Test\Constraint\AssertProductAttributeAbsenceForExport" /> </variation> <variation name="DeleteProductAttributeEntityTestVariation2"> - <data name="tag" xsi:type="string">stable:no</data> <data name="attribute/dataset" xsi:type="string">attribute_type_dropdown</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeSuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeAbsenceInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteSystemProductAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteSystemProductAttributeTest.xml index 7763b0fb534f4..a9a85d2472073 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteSystemProductAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteSystemProductAttributeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteSystemProductAttributeTest" summary="Delete System Product Attribute" ticketId="MAGETWO-24771"> <variation name="DeleteSystemProductAttributeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productAttribute/data/attribute_code" xsi:type="string">news_from_date</data> <data name="productAttribute/data/is_user_defined" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertAbsenceDeleteAttributeButton" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteUsedInConfigurableProductAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteUsedInConfigurableProductAttributeTest.xml index df396c22312b1..e46174de8818f 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteUsedInConfigurableProductAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteUsedInConfigurableProductAttributeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteUsedInConfigurableProductAttributeTest" summary="Delete Used in Configurable Product Attribute" ticketId="MAGETWO-26652"> <variation name="DeleteUsedInConfigurableProductAttributeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">one_variation</data> <constraint name="Magento\Catalog\Test\Constraint\AssertUsedSuperAttributeImpossibleDeleteMessages" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml index 18b9a199cac04..18dc5c983ad79 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\UpdateAttributeSetTest" summary="Update Attribute Set" ticketId="MAGETWO-26251"> <variation name="UpdateAttributeSetTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="attributeSet/data/attribute_set_name" xsi:type="string">AttributeSetEdit1%isolation%</data> <data name="attributeSet/data/group" xsi:type="string">Custom-group%isolation%</data> <data name="attributeSetOriginal/dataset" xsi:type="string">custom_attribute_set</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml index b051d50b4acb6..d6420ff431ce6 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\UpdateProductAttributeEntityTest" summary="Update Product Attribute" ticketId="MAGETWO-23459"> <variation name="UpdateProductAttributeEntityTestVariation1"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_text_field</data> <data name="attribute/data/frontend_label" xsi:type="string">Text_Field_%isolation%</data> @@ -29,7 +28,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_dropdown</data> <data name="attribute/data/frontend_label" xsi:type="string">Dropdown_%isolation%</data> @@ -55,6 +53,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation3"> + <data name="issue" xsi:type="string">MAGETWO-46494: [FT] UpdateProductAttributeEntityTestVariation3 does not actually create an attribute to check</data> + <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">tax_class_id</data> <data name="attribute/data/is_searchable" xsi:type="string">Yes</data> @@ -62,11 +62,11 @@ <data name="cacheTags" xsi:type="array"> <item name="0" xsi:type="string">FPC</item> </data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\PageCache\Test\Constraint\AssertCacheIsRefreshableAndInvalidated" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductByAttribute" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation4" summary="Create product attribute of type Dropdown and check its visibility on frontend in Advanced Search form" ticketId="MAGETWO-12941"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_dropdown</data> <data name="attribute/data/frontend_input" xsi:type="string">Dropdown</data> @@ -76,6 +76,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchAttributeIsAbsent" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation5" summary="Create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form" ticketId="MAGETWO-12941"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_multiple_select</data> <data name="attribute/data/frontend_label" xsi:type="string">Dropdown_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php index 9e246939595ca..b5cd056fb99ad 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php @@ -11,6 +11,7 @@ use Magento\Mtf\Util\Command\File\Export; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\Util\Command\Cli\Cron; /** * Preconditions: @@ -50,22 +51,32 @@ class ExportProductsTest extends Injectable */ private $assertExportProduct; + /** + * Cron command + * + * @var Cron + */ + private $cron; + /** * Inject data. * * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex * @param AssertExportProduct $assertExportProduct + * @param Cron $cron * @return void */ public function __inject( FixtureFactory $fixtureFactory, AdminExportIndex $adminExportIndex, - AssertExportProduct $assertExportProduct + AssertExportProduct $assertExportProduct, + Cron $cron ) { $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; $this->assertExportProduct = $assertExportProduct; + $this->cron = $cron; } /** @@ -83,14 +94,18 @@ public function test( array $exportedFields, array $products ) { + $this->cron->run(); + $this->cron->run(); $products = $this->prepareProducts($products); $this->adminExportIndex->open(); + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData = $this->fixtureFactory->createByCode('exportData', ['dataset' => $exportData]); $exportData->persist(); $this->adminExportIndex->getExportForm()->fill($exportData); $this->adminExportIndex->getFilterExport()->clickContinue(); - + $this->cron->run(); + $this->cron->run(); $this->assertExportProduct->processAssert($export, $exportedFields, $products); } diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml index 40f535cd225a2..be22eab8ac717 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation1" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -27,6 +28,7 @@ </data> </variation> <variation name="ExportProductsTestVariation2" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -43,6 +45,7 @@ <constraint name="Magento\CatalogImportExport\Test\Constraint\AssertExportProductDate" /> </variation> <variation name="ExportProductsTestVariation3" summary="Export simple product with custom attribute" ticketId="MAGETWO-46121"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -58,6 +61,7 @@ </data> </variation> <variation name="ExportProductsTestVariation5" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml index edb0aad954fbb..77e5e2b91d93f 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ImportProductsTest" summary="Import products"> <variation name="ImportProductVariation1" ticketId="MAGETWO-47724" summary="Import Products with Add/Update Behavior"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="import/data" xsi:type="array"> <item name="entity" xsi:type="string">Products</item> <item name="behavior" xsi:type="string">Add/Update</item> @@ -38,6 +39,7 @@ <constraint name="Magento\CatalogImportExport\Test\Constraint\AssertProductsInGrid" /> </variation> <variation name="ImportProductVariation2" ticketId="MAGETWO-47719" summary="Import Products assigned to different websites with Replace Behavior"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="import/data/entity" xsi:type="string">Products</data> <data name="import/data/behavior" xsi:type="string">Replace</data> <data name="import/data/validation_strategy" xsi:type="string">Stop on Error</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml index 659d76eabccbe..4a965d5708947 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest14"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Catalog Price Rule</data> <data name="pageTitle" xsi:type="string">Catalog Price Rule</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php index e2193b799c3be..11a8693723f25 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php @@ -61,7 +61,7 @@ private function createProducts(FixtureFactory $fixtureFactory, $productsData) $searchValue = isset($productData[2]) ? $productData[2] : $productData[1]; if ($this->data === null) { if ($product->hasData($searchValue)) { - $getProperty = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $searchValue))); + $getProperty = 'get' . str_replace('_', '', ucwords($searchValue, '_')); $this->data = $product->$getProperty(); } else { $this->data = $searchValue; diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml index 0437e0a5e999b..8c465544a3283 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\CreateSearchTermEntityTest" summary="Create Search Term" ticketId="MAGETWO-26165"> <variation name="CreateSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerm/data/query_text/value" xsi:type="string">catalogProductSimple::sku</data> <data name="searchTerm/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="searchTerm/data/redirect" xsi:type="string">http://example.com/</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml index a9cc0dfd34f9f..8fdd7ef715521 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\DeleteSearchTermEntityTest" summary="Delete Search Term" ticketId="MAGETWO-26491"> <variation name="DeleteSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerm/dataset" xsi:type="string">default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermSuccessDeleteMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml index 3bf4e521c4a04..3ef2b65c0224b 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\MassDeleteSearchTermEntityTest" summary="Mass Delete Search Term" ticketId="MAGETWO-26599"> <variation name="MassDeleteSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerms" xsi:type="string">catalogSearchQuery::default,catalogSearchQuery::default,catalogSearchQuery::default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermSuccessMassDeleteMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermMassActionsNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml index 493d427dd0ac2..d9bb0f65e704e 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest15"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Search Terms</data> <data name="pageTitle" xsi:type="string">Search Terms</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest16"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Search Terms</data> <data name="pageTitle" xsi:type="string">Search Terms Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml index 9a6a66091d427..42dd7b6c96e2e 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml @@ -8,66 +8,76 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\SearchEntityResultsTest" summary="Use Quick Search to Find Product" ticketId="MAGETWO-25095"> <variation name="SearchEntityResultsTestVariation1" summary="Use Quick Search to Find the Product" ticketId="MAGETWO-12420"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::sku</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation2" summary="Search simple product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::simple</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation3" summary="Search virtual product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductVirtual::default::virtual</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation4" summary="Search configurable product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">configurableProduct::default::configurable</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation5" summary="Search downloadable product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">downloadableProduct::default::downloadable</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation6" summary="Search grouped product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">groupedProduct::withSimpleProducts_without_qty::grouped</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation7" summary="Search bundle dynamic product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">bundleProduct::bundle_dynamic_product::bundle</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation8" summary="Search fixed product"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">bundleProduct::bundle_fixed_product::bundle</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::name</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::product_with_special_symbols_in_name::name</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/search_query" xsi:type="string">TryToFindMeAndI'llFindYOU</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResultMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResult" /> </variation> <variation name="SearchEntityResultsTestVariation12" summary="Search for simple product name using 2 symbols query length" ticketId="MAGETWO-36542"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::name</data> <data name="queryLength" xsi:type="string">2</data> - <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResultMessage" /> - <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResult" /> + <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation13" summary="Search for simple product name using 3 symbols query length" ticketId="MAGETWO-36542"> - <data name="issue" xsi:type="string">MAGETWO-65509: [FT] Magento\CatalogSearch\Test\TestCase\SearchEntityResultsTest fails on Jenkins</data> <data name="tag" xsi:type="string">stable:no</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::name</data> <data name="queryLength" xsi:type="string">3</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation14" summary="Search for simple product name using 128 symbols query length" ticketId="MAGETWO-36542"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::product_with_long_name::name</data> <data name="queryLength" xsi:type="string">128</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> @@ -82,6 +92,7 @@ <variation name="SearchEntityResultsTestVariation16" summary="Search for two simple products for text in attributes with same search weight and check their sort order" ticketId="MAGETWO-64501"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/search_query" xsi:type="string">alaska</data> <data name="catalogSearch/data/query_text/value" xsi:type="array"> <item name="product_2" xsi:type="string">catalogProductSimple::search_weight_term_twice_weight_1</item> @@ -92,6 +103,7 @@ <variation name="SearchEntityResultsTestVariation17" summary="Search for two simple products for text in attributes with different search weight and check their sort order" ticketId="MAGETWO-64502"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/search_query" xsi:type="string">alaska</data> <data name="catalogSearch/data/query_text/value" xsi:type="array"> <item name="product_1" xsi:type="string">catalogProductSimple::search_weight_term_once_weight_5</item> @@ -100,6 +112,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchResultOrder" /> </variation> <variation name="SearchEntityResultsTestVariation18" summary="Search Configurable Product with Enabled and Disabled Children." ticketId="MAGETWO-69181"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">configurableProduct::one_simple_product_not_visible_individually::name</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchResult" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertConfigurableWithDisabledOptionCatalogSearchNoResult" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml index 398054f1f0ed3..8b15da5ecd2ef 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml @@ -14,7 +14,7 @@ <data name="category/data/include_in_menu" xsi:type="string">Yes</data> <data name="category/data/name" xsi:type="string">Subcategory%isolation%</data> <data name="category/data/url_key" xsi:type="string">subcategory-%isolation%</data> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1, mftf_migrated:yes</data> <constraint name="Magento\CatalogUrlRewrite\Test\Constraint\AssertCategoryUrlDuplicateErrorMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml index 1116821f756a9..8110ed1ed00b1 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogUrlRewrite\Test\TestCase\CreateDuplicateUrlProductEntity" summary="Create Simple Product" ticketId="MAGETWO-69427"> <variation name="CreateDuplicateUrlProductEntityTestVariation1" summary="Create Duplicate Url Product"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1, mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index f632cdc3d7464..1d3950091d064 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -117,6 +117,7 @@ public function getGrandTotal() */ public function getGrandTotalIncludingTax() { + $this->waitForGrandTotal(); $priceElement = $this->_rootElement->find($this->grandTotalInclTax, Locator::SELECTOR_CSS); return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php index 0bf9b37c75e12..1013404f42df1 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php @@ -102,7 +102,7 @@ class Shipping extends Form * * @var string */ - private $emailError = '#customer-email-error'; + private $emailError = '#checkout-customer-email-error'; /** * Get email error. diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml index 71115e402880c..0973b968cba95 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml @@ -8,7 +8,7 @@ <mapping strict="0"> <fields> <email> - <selector>#customer-email</selector> + <selector>#checkout-customer-email</selector> </email> <firstname /> <lastname /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml index 8d054c0230873..c0df4b16b92e6 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml @@ -22,7 +22,6 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S2</data> <data name="productsData/0" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="cart/data/grand_total" xsi:type="string">761</data> <data name="cart/data/subtotal" xsi:type="string">756</data> @@ -35,7 +34,6 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation3"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S0</data> <data name="productsData/0" xsi:type="string">catalogProductSimple::with_two_custom_option</data> <data name="cart/data/grand_total" xsi:type="string">345</data> <data name="cart/data/subtotal" xsi:type="string">340</data> @@ -48,7 +46,6 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation4"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S1</data> <data name="productsData/0" xsi:type="string">catalogProductVirtual::product_50_dollar</data> <data name="cart/data/grand_total" xsi:type="string">50</data> <data name="cart/data/subtotal" xsi:type="string">50</data> @@ -100,8 +97,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation8" summary="Enable https in backend and add products of different types to cart" ticketId="MAGETWO-42677"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S0</data> - <data name="issue" xsi:type="string">Errors on configuration step. Skipped.</data> + <data name="tag" xsi:type="string">severity:S0</data> <data name="productsData/0" xsi:type="string">catalogProductSimple::with_two_custom_option</data> <data name="productsData/1" xsi:type="string">catalogProductVirtual::product_50_dollar</data> <data name="productsData/2" xsi:type="string">downloadableProduct::with_two_separately_links</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml index 3c88d9193db28..64ed05904469e 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\EditShippingAddressOnePageCheckoutTest" summary="Customer can place order with new addresses that was edited during checkout with several conditions" ticketId="MAGETWO-67837"> <variation name="EditShippingAddressOnePageCheckoutTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">johndoe_with_addresses</data> <data name="shippingAddress/dataset" xsi:type="string">UK_address_without_email</data> <data name="editShippingAddress/dataset" xsi:type="string">empty_UK_address_without_email</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml index 6635c1edbe78d..75603d12cbe32 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\OnePageCheckoutJsValidationTest" summary="JS validation verification for Checkout flow" ticketId="MAGETWO-59697"> <variation name="OnePageCheckoutJsValidationTestVariation1" summary="JS validation is not applied for empty required checkout fields if customer did not fill them"> + <data name="issue" xsi:type="string">MAGETWO-97990: [MTF] OnePageCheckoutJsValidationTestVariation1_0 randomly fails on jenkins</data> <data name="tag" xsi:type="string">severity:S2</data> <data name="products/0" xsi:type="string">catalogProductSimple::default</data> <data name="checkoutMethod" xsi:type="string">guest</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml index 0edd8f4183f30..daf9aaae50580 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml @@ -15,13 +15,11 @@ <data name="shippingAddressCustomer" xsi:type="array"> <item name="added" xsi:type="number">1</item> </data> - <data name="billingAddressCustomer" xsi:type="array"> - <item name="added" xsi:type="number">1</item> - </data> <data name="prices" xsi:type="array"> <item name="grandTotal" xsi:type="string">565.00</item> </data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> @@ -43,6 +41,7 @@ <item name="grandTotal" xsi:type="string">565.00</item> </data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> @@ -61,7 +60,7 @@ <data name="shippingAddress/dataset" xsi:type="string">UK_address_without_email</data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="billingCheckboxState" xsi:type="string">Yes</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="billingAddress/dataset" xsi:type="string">US_address_1_without_email</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml index 8b2460718097c..4b99de09f2a7b 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -9,7 +9,7 @@ <testCase name="Magento\Checkout\Test\TestCase\UpdateProductFromMiniShoppingCartEntityTest" summary="Update Product from Mini Shopping Cart" ticketId="MAGETWO-29812"> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation1" summary="Update Product Qty on Mini Shopping Cart" ticketId=" MAGETWO-35536"> <data name="issue" xsi:type="string">https://github.com/magento-engcom/msi/issues/1624</data> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0, mftf_migrated:yes</data> <data name="originalProduct/0" xsi:type="string">catalogProductSimple::default</data> <data name="checkoutData/dataset" xsi:type="string">simple_order_qty_2</data> <data name="use_minicart_to_edit_qty" xsi:type="boolean">true</data> @@ -26,7 +26,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertCustomerIsRedirectedToCheckoutFromCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation2" summary="Update Configurable and verify previous product was updated to new one in shopping cart and mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S0</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> <data name="originalProduct/0" xsi:type="string">configurableProduct::default</data> <data name="checkoutData/dataset" xsi:type="string">configurable_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCartItemsOptions" /> @@ -35,7 +35,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductOptionsAbsentInShoppingCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation3" summary="Update Bundle and verify previous product was updated to new one in shopping cart and mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S0</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> <data name="originalProduct/0" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="checkoutData/dataset" xsi:type="string">bundle_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCartItemsOptions" /> @@ -44,7 +44,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductOptionsAbsentInShoppingCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation4" summary="Update Downloadable and check previous link was updated to new one in shopping cart and mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S1</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S1</data> <data name="originalProduct/0" xsi:type="string">downloadableProduct::with_two_separately_links</data> <data name="checkoutData/dataset" xsi:type="string">downloadable_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCartItemsOptions" /> @@ -53,7 +53,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductOptionsAbsentInShoppingCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation5" summary="Update Virtual product in mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S1</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S1</data> <data name="originalProduct/0" xsi:type="string">catalogProductVirtual::default</data> <data name="checkoutData/dataset" xsi:type="string">virtual_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertProductDataInMiniShoppingCart" /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml index e0ea721a51f1b..5caa3ba9b924e 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\UpdateShoppingCartTest" summary="Update Shopping Cart" ticketId="MAGETWO-25081"> <variation name="UpdateShoppingCartTestVariation1"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0,mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">default</data> <data name="product/data/price/value" xsi:type="string">100</data> <data name="product/data/checkout_data/qty" xsi:type="string">3</data> @@ -20,7 +20,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInShoppingCart" /> </variation> <variation name="UpdateShoppingCartTestVariation2"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0,mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">with_two_custom_option</data> <data name="product/data/price/value" xsi:type="string">50</data> <data name="product/data/checkout_data/qty" xsi:type="string">11</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml index 90c42505585a2..8290d825593af 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml @@ -10,17 +10,20 @@ <variation name="ValidateEmailOnCheckoutTestVariation1"> <data name="customer/data/email" xsi:type="string">johndoe</data> <data name="customer/data/firstname" xsi:type="string">John</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailErrorValidationMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailToolTips" /> </variation> <variation name="ValidateEmailOnCheckoutTestVariation2"> <data name="customer/data/email" xsi:type="string">johndoe#example.com</data> <data name="customer/data/firstname" xsi:type="string">John</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailErrorValidationMessage" /> </variation> <variation name="ValidateEmailOnCheckoutTestVariation3"> <data name="customer/data/email" xsi:type="string">johndoe@example.c</data> <data name="customer/data/firstname" xsi:type="string">John</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailErrorValidationMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php index aa7eba634145f..52b296c2e01fa 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php @@ -126,11 +126,15 @@ public function run() if ($this->billingCheckboxState) { $this->assertBillingAddressCheckbox->processAssert($this->checkoutOnepage, $this->billingCheckboxState); } - if ($this->billingCheckboxState === 'Yes' && !$this->editBillingInformation) { - return [ - 'billingAddress' => $this->shippingAddress - ]; + + if (!$this->editBillingInformation) { + $billingAddress = $this->billingCheckboxState === 'Yes' + ? $this->shippingAddress + : $this->getDefaultBillingAddress(); + + return ['billingAddress' => $billingAddress]; } + if ($this->billingAddress) { $selectedPaymentMethod = $this->checkoutOnepage->getPaymentBlock()->getSelectedPaymentMethodBlock(); if ($this->shippingAddress) { @@ -139,9 +143,11 @@ public function run() $selectedPaymentMethod->getBillingBlock()->fillBilling($this->billingAddress); $billingAddress = $this->billingAddress; } + if (isset($this->billingAddressCustomer['added'])) { $addressIndex = $this->billingAddressCustomer['added']; - $billingAddress = $this->customer->getDataFieldConfig('address')['source']->getAddresses()[$addressIndex]; + $billingAddress = $this->customer->getDataFieldConfig('address')['source'] + ->getAddresses()[$addressIndex]; $address = $this->objectManager->create( \Magento\Customer\Test\Block\Address\Renderer::class, ['address' => $billingAddress, 'type' => 'html_for_select_element'] @@ -156,4 +162,25 @@ public function run() 'billingAddress' => $billingAddress ]; } + + /** + * Get default billing address + * + * @return Address|null + */ + private function getDefaultBillingAddress() + { + $addresses = $this->customer->hasData('address') + ? $this->customer->getDataFieldConfig('address')['source']->getAddresses() + : []; + $defaultAddress = null; + foreach ($addresses as $address) { + if ($address->getDefaultBilling() === 'Yes') { + $defaultAddress = $address; + break; + } + } + + return $defaultAddress; + } } diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml index d304d305a7265..a266b09278ddb 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> <page name="MultishippingCheckoutOverview" mca="multishipping/checkout/overview" module="Magento_Checkout"> - <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator="#checkout-agreements" strategy="css selector"/> + <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator=".checkout-agreements" strategy="css selector"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml index 18cbf32ded5c1..3458e2944a9ec 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest17"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Terms and Conditions</data> <data name="pageTitle" xsi:type="string">Terms and Conditions</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml index 72a76dacc3297..06fe76c5efd0e 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Cms\Test\TestCase\CreateCmsPageEntityMultipleStoreViewsTest" summary="Page cache for different CMS pages on multiple store views" ticketId="MAGETWO-52467"> <variation name="CreateCmsPageEntityMultipleStoreViewsTestVariation1"> + <data name="issue" xsi:type="string">MC-13801: Test "Page cache for different CMS pages on multiple store views" fails on Jenkins</data> <data name="cmsPages/0/is_active" xsi:type="string">Yes</data> <data name="cmsPages/0/title" xsi:type="string">NewCmsPage</data> <data name="cmsPages/0/store_id/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml index 3682fce4267c6..8adbc1ae46c7b 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CreateCustomUrlRewriteEntityTest" summary="Create Custom URL Rewrites" ticketId="MAGETWO-25474"> <variation name="CreateCustomUrlRewriteEntityTestVariation3"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">Custom</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">cms/page/view/page_id/%cmsPage::default%</data> @@ -19,7 +19,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCustomRedirect" /> </variation> <variation name="CreateCustomUrlRewriteEntityTestVariation4"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">Custom</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">cms/page/view/page_id/%cmsPage::default%</data> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml index f9f0a11c0a475..cff5f7f2a5622 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml @@ -8,13 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest18"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Pages</data> <data name="pageTitle" xsi:type="string">Pages</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest19"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Blocks</data> <data name="pageTitle" xsi:type="string">Blocks</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml index c157f5c58d408..0a2ce7ab7f183 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml @@ -7,7 +7,8 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> - <variation name="ExportProductsTestVariation1" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <variation name="ExportProductsTestVariation7" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> <item name="dataset" xsi:type="string">product_with_size</item> @@ -18,19 +19,48 @@ </item> </item> </data> + <data name="exportedFields" xsi:type="array"> + <item name="0" xsi:type="string">sku</item> + <item name="1" xsi:type="string">name</item> + <item name="2" xsi:type="string">weight</item> + <item name="3" xsi:type="string">visibility</item> + <item name="4" xsi:type="string">price</item> + <item name="5" xsi:type="string">url_key</item> + <item name="6" xsi:type="string">additional_images</item> + </data> </variation> - <variation name="ExportProductsTestVariation2" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <variation name="ExportProductsTestVariation8" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> <item name="dataset" xsi:type="string">first_product_with_custom_options_and_option_key_1</item> </data> + <data name="exportedFields" xsi:type="array"> + <item name="0" xsi:type="string">sku</item> + <item name="1" xsi:type="string">name</item> + <item name="2" xsi:type="string">weight</item> + <item name="3" xsi:type="string">visibility</item> + <item name="4" xsi:type="string">price</item> + <item name="5" xsi:type="string">url_key</item> + </data> </variation> - <variation name="ExportProductsTestVariation5" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> + <variation name="ExportProductsTestVariation9" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> <item name="dataset" xsi:type="string">default</item> <item name="store" xsi:type="string">custom_store</item> </data> + <data name="exportedFields" xsi:type="array"> + <item name="0" xsi:type="string">sku</item> + <item name="1" xsi:type="string">name</item> + <item name="2" xsi:type="string">weight</item> + <item name="3" xsi:type="string">visibility</item> + <item name="4" xsi:type="string">price</item> + <item name="5" xsi:type="string">url_key</item> + </data> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml index f7bd155fd2d51..d89fb3ddf88a5 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml @@ -8,7 +8,7 @@ <mapping strict="0"> <fields> <attribute> - <selector>//div[@class="product-options"]//label[.="%s"]//following-sibling::*//select</selector> + <selector>//div[contains(@class, "product-options")]//div//label[.="%s"]//following-sibling::*//select</selector> <strategy>xpath</strategy> <input>select</input> </attribute> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml index e1b6fcf47e562..f831173ba0ae0 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\ConfigurableProduct\Test\TestCase\CreateConfigurableProductEntityTest" summary="Create Configurable Product" ticketId="MAGETWO-26041"> <variation name="CreateConfigurableProductEntityTestVariation1" summary="Create product with category and two new options"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_new_options</data> <data name="product/data/checkout_data/dataset" xsi:type="string">configurable_two_options</data> @@ -32,6 +33,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately"/> </variation> <variation name="CreateConfigurableProductEntityTestVariation2" summary="Create product with two options"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_options</data> <data name="product/data/checkout_data/dataset" xsi:type="string">configurable_two_options</data> @@ -97,6 +99,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertConfigurableProductInCart" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation5" summary="Create Configurable Product and Assign it to Category" ticketId="MAGETWO-12620"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_options_with_fixed_price</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> @@ -113,7 +116,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertConfigurableProductPage" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation6" summary="Create Configurable Product with Creating New Category and New Attribute (Required Fields Only)" ticketId="MAGETWO-13361"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_searchable_options</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">configurable_sku_%isolation%</data> @@ -129,6 +132,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertConfigurableProductPage" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation7" summary="Verify that variation's SKU based on parent SKU"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_new_options_with_empty_sku</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">configurable_sku_%isolation%</data> @@ -137,6 +141,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsGeneratedSku" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation8" summary="Assert notice that existing sku automatically changed when saving product with same sku"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_new_options_with_parent_sku</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">existing_sku</data> @@ -145,6 +150,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAutoincrementedSkuNoticeMessage" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation9" summary="Create configurable product and assign it to custom website"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_options_with_assigned_product_special_price</data> <data name="product/data/checkout_data/dataset" xsi:type="string">configurable_two_new_options_with_special_price</data> @@ -156,6 +162,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductOnCustomWebsite" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation10" summary="Create configurable product with tier price for one item"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_options_with_assigned_product_tier_price</data> <data name="product/data/checkout_data/dataset" xsi:type="string">configurable_two_new_options_with_special_price</data> @@ -170,7 +177,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertProductTierPriceOnProductPage" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation11" summary="Create Configurable Product with out of stock child" ticketId="MAGETWO-65660"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">with_out_of_stock_item</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">configurable_sku_%isolation%</data> @@ -189,7 +196,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertConfigurableProductOutOfStockPage" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation12" summary="Create Configurable Product with disabled child" ticketId="MAGETWO-65661"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">with_disabled_item</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">configurable_sku_%isolation%</data> @@ -207,7 +214,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductNotVisibleInCategory" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation13" summary="Create Configurable Product with one disabled child and with one out of stock child" ticketId="MAGETWO-65662"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">with_one_disabled_item_and_one_out_of_stock_item</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">configurable_sku_%isolation%</data> @@ -225,6 +232,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation14" summary="Create configurable product with images" ticketId="MAGETWO-41354"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">color_and_size_with_images</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> <data name="product/data/sku" xsi:type="string">configurable_sku_%isolation%</data> @@ -238,6 +246,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertConfigurableProductImages" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation15" summary="Create Configurable Product with 1 out of stock and several in stock options with displaying out of stock ones" ticketId="MAGETWO-89274"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">three_new_options_with_out_of_stock_product</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> @@ -254,6 +263,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertOutOfStockOptionIsAbsentOnProductPage" /> </variation> <variation name="CreateConfigurableProductEntityTestVariation16" summary="Create Configurable Product with 1 out of stock and several in stock options" ticketId="MAGETWO-69508"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">three_new_options_with_out_of_stock_product</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteChildConfigurableProductTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteChildConfigurableProductTest.xml index 435d5aad4635d..64f9141fba962 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteChildConfigurableProductTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteChildConfigurableProductTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\ConfigurableProduct\Test\TestCase\DeleteChildConfigurableProductTest" summary="Configurable Product is not available on frontend after child products are deleted" ticketId="MAGETWO-70346"> <variation name="DeleteChildConfigurableProductTestVariation1" summary="Verify that variation's SKU based on parent SKU"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_new_options_with_empty_sku</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml index 25f31b23fa665..68dc1ecbe787e 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">configurableProduct::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> @@ -15,6 +16,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">configurableProduct::with_one_option</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php index c7d66781738b7..bb88bc854f756 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php @@ -32,7 +32,6 @@ class UpdateConfigurableProductEntityTest extends Scenario { /* tags */ const MVP = 'yes'; - const TO_MAINTAIN = 'yes'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml index a62bcf92ebf39..5af854cae5434 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/1" xsi:type="string">Configurable Product</data> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductEntityPriceTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductEntityPriceTest.xml index 6d22cea4689a8..d576e760179ed 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductEntityPriceTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductEntityPriceTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\ConfigurableProduct\Test\TestCase\VerifyConfigurableProductEntityPriceTest" summary="Verify price for configurable product"> <variation name="VerifyConfigurableProductEntityPriceTestVariation1" summary="Disable child product" ticketId="MAGETWO-60196"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product" xsi:type="string">configurableProduct::product_with_color</data> <data name="productUpdate/childProductUpdate" xsi:type="array"> <item name="data" xsi:type="array"> @@ -19,6 +20,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertConfigurableProductPage" /> </variation> <variation name="VerifyConfigurableProductEntityPriceTestVariation2" summary="Set child product Out of stock" ticketId="MAGETWO-60206"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product" xsi:type="string">configurableProduct::product_with_color</data> <data name="productUpdate/childProductUpdate" xsi:type="array"> <item name="data" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductLayeredNavigationTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductLayeredNavigationTest.xml index 082b93ba62e03..9108b44a0e85b 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductLayeredNavigationTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductLayeredNavigationTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\ConfigurableProduct\Test\TestCase\VerifyConfigurableProductLayeredNavigationTest" summary="Verify OOS option configurable product in Layered Navigation on storefront"> <variation name="VerifyConfigurableProductLayeredNavigationTestVariation1" summary="Verify the out of stock configurable attribute option doesn't show in Layered navigation" ticketId="MAGETWO-89745"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product" xsi:type="string">configurableProduct::product_with_3_sizes</data> <data name="productUpdate/childProductUpdate" xsi:type="array"> <item name="data" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml index 0a061eb4be6c7..fc031a77ff53f 100644 --- a/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest20"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Currency Rates</data> <data name="pageTitle" xsi:type="string">Currency Rates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest21"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Currency Symbols</data> <data name="pageTitle" xsi:type="string">Currency Symbols</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php index cba358549c3d7..13ef742d0627c 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php @@ -20,14 +20,14 @@ class AddressesAdditional extends Block * * @var string */ - protected $addressSelector = '//li[address[contains(.,"%s")]]'; + protected $addressSelector = '//tbody//tr[contains(.,"%s")]'; /** * Selector for addresses block * * @var string */ - protected $addressesSelector = '//li[address]'; + protected $addressesSelector = '.additional-addresses'; /** * Selector for delete link @@ -74,16 +74,19 @@ public function deleteAdditionalAddress(Address $address) */ public function isAdditionalAddressExists($address) { - $additionalAddressExists = false; - - $addresses = $this->_rootElement->getElements($this->addressesSelector, Locator::SELECTOR_XPATH); - foreach ($addresses as $addressBlock) { - if (strpos($addressBlock->getText(), $address) === 0) { - $additionalAddressExists = $addressBlock->isVisible(); + $addressExists = true; + foreach (explode("\n", $address) as $addressItem) { + $addressElement = $this->_rootElement->find( + sprintf($this->addressSelector, $addressItem), + Locator::SELECTOR_XPATH + ); + if (!$addressElement->isVisible()) { + $addressExists = false; break; } } - return $additionalAddressExists; + + return $addressExists; } /** diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php index f9a3989d8f574..8d8a0cfe5ea1a 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php @@ -58,6 +58,11 @@ protected function getPattern() . "{{lastname}}{{depend}} {{suffix}}{{/depend}}\n{{/depend}}{{street}}\n" . "{{city}}, {{{$region}}} {{postcode}}\n{{country_id}}\n{{depend}}{{telephone}}{{/depend}}"; break; + case "html_without_company_separated_names": + $outputPattern = "{{depend}}{{prefix}}\n{{/depend}}{{firstname}}\n{{depend}}{{middlename}}\n{{/depend}}" + . "{{lastname}}{{depend}}\n{{suffix}}{{/depend}}\n{{/depend}}{{street}}\n" + . "{{city}}\n{{{$region}}}\n{{postcode}}\n{{country_id}}\n{{depend}}{{telephone}}{{/depend}}"; + break; case "html_for_select_element": $outputPattern = "{{depend}}{{prefix}} {{/depend}}{{firstname}} {{depend}}{{middlename}} {{/depend}}" . "{{lastname}}{{depend}} {{suffix}}{{/depend}}, {{/depend}}{{street}}, " diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php index abfee067a73de..4d086cf06053d 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php @@ -28,7 +28,7 @@ public function processAssert(CustomerAccountIndex $customerAccountIndex, Addres $customerAccountIndex->getAccountMenuBlock()->openMenuItem('Address Book'); $addressRenderer = $this->objectManager->create( \Magento\Customer\Test\Block\Address\Renderer::class, - ['address' => $shippingAddress, 'type' => 'html'] + ['address' => $shippingAddress, 'type' => 'html_without_company_separated_names'] )->render(); $isAddressExists = $customerAccountIndex->getAdditionalAddressBlock() ->isAdditionalAddressExists($addressRenderer); diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ChangeCustomerPasswordTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ChangeCustomerPasswordTest.xml index 9765b9d9340a7..f94d99b07ec6e 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ChangeCustomerPasswordTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ChangeCustomerPasswordTest.xml @@ -9,6 +9,7 @@ <testCase name="Magento\Customer\Test\TestCase\ChangeCustomerPasswordTest" summary="Change Customer Password from My Account" ticketId="MAGETWO-29411"> <variation name="ChangeCustomerPasswordTestVariation1"> <data name="initialCustomer/dataset" xsi:type="string">default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/current_password" xsi:type="string">123123^q</data> <data name="customer/data/password" xsi:type="string">123123^a</data> <data name="customer/data/password_confirmation" xsi:type="string">123123^a</data> @@ -17,6 +18,7 @@ </variation> <variation name="ChangeCustomerPasswordTestVariation2"> <data name="initialCustomer/dataset" xsi:type="string">default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/current_password" xsi:type="string">123123</data> <data name="customer/data/password" xsi:type="string">123123^a</data> <data name="customer/data/password_confirmation" xsi:type="string">123123^a</data> @@ -24,6 +26,7 @@ </variation> <variation name="ChangeCustomerPasswordTestVariation3"> <data name="initialCustomer/dataset" xsi:type="string">default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/current_password" xsi:type="string">123123^q</data> <data name="customer/data/password" xsi:type="string">123123^a</data> <data name="customer/data/password_confirmation" xsi:type="string">123123^d</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml index 5bb96dc13c739..ff87a1df60fe8 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\CreateCustomerBackendEntityTest" summary="Create Customer from Backend" ticketId="MAGETWO-23424"> <variation name="CreateCustomerBackendEntityTestVariation1" summary="General customer without address"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerAction" xsi:type="string">save</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">General</data> @@ -35,7 +36,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation3" summary="General customer from USA"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="customerAction" xsi:type="string">save</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">General</data> @@ -55,6 +56,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation4" summary="Retailer customer without address"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerAction" xsi:type="string">save</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">Retailer</data> @@ -64,6 +66,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerInvalidEmail" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation5" summary="General customer from Poland"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerAction" xsi:type="string">save</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">General</data> @@ -83,6 +86,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation6" summary="Create New Customer on Backend" ticketId="MAGETWO-12516"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerAction" xsi:type="string">saveAndContinue</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">General</data> @@ -105,6 +109,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation8" summary="Verify required fields on Account Information tab."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerAction" xsi:type="string">save</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">General</data> @@ -116,6 +121,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerBackendRequiredFields" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation9" summary="Verify required fields on the Add address modal."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/website_id" xsi:type="string">Main Website</data> <data name="customer/data/group_id/dataset" xsi:type="string">General</data> <data name="customer/data/firstname" xsi:type="string">John%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerBackendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerBackendEntityTest.xml index 77b63fe39d4a1..3807360e89ec0 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerBackendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerBackendEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\DeleteCustomerBackendEntityTest" summary="Delete Customer Backend Entity" ticketId="MAGETWO-24764"> <variation name="DeleteCustomerBackendEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">default</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerSuccessDeleteMessage" /> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml index cffbbca8ad5cb..4251a1c5ee9c5 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\DeleteCustomerGroupEntityTest" summary="Delete Customer Group" ticketId="MAGETWO-25243"> <variation name="DeleteCustomerGroupEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">customer_with_new_customer_group</data> <data name="customer/data/group_id/dataset" xsi:type="string">default</data> <data name="defaultCustomerGroup/dataset" xsi:type="string">General</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ForgotPasswordOnFrontendTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ForgotPasswordOnFrontendTest.xml index 0d168451c6d32..7bf916157c5dc 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ForgotPasswordOnFrontendTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ForgotPasswordOnFrontendTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\ForgotPasswordOnFrontendTest" summary="Forgot customer password on frontend" ticketId="MAGETWO-37145"> <variation name="ForgotPasswordOnFrontendTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">customer_US</data> <data name="configData" xsi:type="string">captcha_storefront_disable</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForgotPasswordSuccessMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/LoginOnFrontendFailTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/LoginOnFrontendFailTest.xml index 2dea52c1b7fcc..bf84d2be43a11 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/LoginOnFrontendFailTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/LoginOnFrontendFailTest.xml @@ -9,6 +9,7 @@ <testCase name="Magento\Customer\Test\TestCase\LoginOnFrontendFailTest" summary="Check error message with wrong credentials" ticketId="MAGETWO-16883"> <variation name="LoginOnFrontendFailTestVariation1"> <data name="customer/dataset" xsi:type="string">default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerLoginErrorMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/MassAssignCustomerGroupTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/MassAssignCustomerGroupTest.xml index 4086a8585c8a8..c868d3332a5a9 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/MassAssignCustomerGroupTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/MassAssignCustomerGroupTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\MassAssignCustomerGroupTest" summary="Mass Assign Customer's Group to Customers" ticketId="MAGETWO-27892"> <variation name="MassAssignCustomerGroupTestVariation1" summary="Customer is created and mass action for changing customer group to created group is applied"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customers" xsi:type="array"> <item name="0" xsi:type="string">default</item> </data> @@ -16,6 +17,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerGroupInGrid" /> </variation> <variation name="MassAssignCustomerGroupTestVariation2" summary="Two customers are created and mass actions for changing customer group to 'Retail' is applied" ticketId="MAGETWO-19456"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customers" xsi:type="array"> <item name="0" xsi:type="string">default</item> <item name="1" xsi:type="string">customer_US</item> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml index 404e62dcad648..a4a3aa34f9f1c 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml @@ -8,16 +8,19 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest22"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Customers > All Customers</data> <data name="pageTitle" xsi:type="string">Customers</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest23"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Customers > Now Online</data> <data name="pageTitle" xsi:type="string">Customers Now Online</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest24"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Customers > Customer Groups</data> <data name="pageTitle" xsi:type="string">Customer Groups</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/PasswordAutocompleteOffTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/PasswordAutocompleteOffTest.xml index b4188ebd98ae7..7f267c1f171f6 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/PasswordAutocompleteOffTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/PasswordAutocompleteOffTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\PasswordAutocompleteOffTest" summary="Test that autocomplete is off" ticketId="MAGETWO-45324"> <variation name="RegisterCustomerFrontendEntityTestVariation1" summary="Test that autocomplete is off"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">default</data> <data name="configData" xsi:type="string">disable_guest_checkout,password_autocomplete_off</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerPasswordAutocompleteOnAuthorizationPopup" /> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/RegisterCustomerFrontendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/RegisterCustomerFrontendEntityTest.xml index c1cf533583114..86c8e56d232d1 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/RegisterCustomerFrontendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/RegisterCustomerFrontendEntityTest.xml @@ -20,6 +20,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerLogout" /> </variation> <variation name="RegisterCustomerFrontendEntityTestVariation2" summary="Register new customer with subscribing"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">john</data> <data name="customer/data/lastname" xsi:type="string">doe</data> <data name="customer/data/email" xsi:type="string">johndoe%isolation%@example.com</data> @@ -32,7 +33,7 @@ <constraint name="Magento\Newsletter\Test\Constraint\AssertCustomerIsSubscribedToNewsletter" /> </variation> <variation name="RegisterCustomerFrontendEntityTestVariation3" summary="Register Customer" ticketId="MAGETWO-12394"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">john</data> <data name="customer/data/lastname" xsi:type="string">doe</data> <data name="customer/data/email" xsi:type="string">johndoe%isolation%@example.com</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml index 24f68f686fdc1..095746cfdef57 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerBackendEntityTest.xml @@ -25,6 +25,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerInGrid" /> </variation> <variation name="UpdateCustomerBackendEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCustomer/dataset" xsi:type="string">default</data> <data name="customer/data/email" xsi:type="string">-</data> <data name="address/data/prefix" xsi:type="string">Prefix%isolation%_</data> @@ -75,6 +76,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerInGrid" /> </variation> <variation name="UpdateCustomerBackendEntityTestVariation4" summary="Address w/o zip/state required, default billing/shipping checked"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCustomer/dataset" xsi:type="string">default</data> <data name="customer/data/email" xsi:type="string">-</data> <data name="address/data/default_billing" xsi:type="string">Yes</data> @@ -95,6 +97,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerLogin" /> </variation> <variation name="UpdateCustomerBackendEntityTestVariation5" summary="Default billing/shipping unchecked"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCustomer/dataset" xsi:type="string">johndoe_unique_TX</data> <data name="customer/data/email" xsi:type="string">-</data> <data name="address/data/default_billing" xsi:type="string">No</data> @@ -114,6 +117,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="UpdateCustomerBackendEntityTestVariation6" summary="Delete customer address"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCustomer/dataset" xsi:type="string">johndoe_with_multiple_addresses</data> <data name="customer/data/email" xsi:type="string">-</data> <data name="addressIndexToDelete" xsi:type="number">1</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerFrontendEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerFrontendEntityTest.xml index 2cbb9a0315e16..b96d688b0c4b6 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerFrontendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerFrontendEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\UpdateCustomerFrontendEntityTest" summary="Update Customer on Frontend" ticketId="MAGETWO-25925"> <variation name="UpdateCustomerFrontendEntityTestVariation1" summary="No XSS injection on update customer information add address" ticketId="MAGETWO-47189"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">Patrick</title></head><svg/onload=alert('XSS')></data> <data name="customer/data/lastname" xsi:type="string"><script>alert('Last name')</script></data> <data name="customer/data/current_password" xsi:type="string">123123^q</data> @@ -25,6 +26,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="UpdateCustomerFrontendEntityTestVariation2" summary="Update customer information and add UK address, assert customer name and address on address book tab"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">Jonny %isolation%</data> <data name="customer/data/lastname" xsi:type="string">Doe %isolation%</data> <data name="customer/data/email" xsi:type="string">jonny%isolation%@example.com</data> @@ -44,6 +46,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerNameFrontend" /> </variation> <variation name="UpdateCustomerFrontendEntityTestVariation3" summary="Update customer information and add France address"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">Jean %isolation%</data> <data name="customer/data/lastname" xsi:type="string">Reno %isolation%</data> <data name="customer/data/email" xsi:type="string">jean%isolation%@example.com</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml index 70a912a3b5ffe..e88e5161e474e 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\VerifyDisabledCustomerGroupFieldTest" summary="Check that field is disabled in system Customer Group" ticketId="MAGETWO-52481"> <variation name="VerifyDisabledCustomerGroupField1" summary="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerGroup/dataset" xsi:type="string">NOT_LOGGED_IN</data> <data name="disabledFields" xsi:type="array"> <item name="0" xsi:type="string">customer_group_code</item> diff --git a/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php b/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php index 1f046f5111dfe..17dfb4fb8cdaf 100644 --- a/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php +++ b/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php @@ -10,6 +10,7 @@ use Magento\ImportExport\Test\Page\Adminhtml\AdminExportIndex; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\Util\Command\Cli\Cron; /** * Preconditions: @@ -42,19 +43,29 @@ class ExportCustomerAddressesTest extends Injectable */ private $adminExportIndex; + /** + * Cron command + * + * @var Cron + */ + private $cron; + /** * Inject pages. * * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex + * @param Cron $cron * @return void */ public function __inject( FixtureFactory $fixtureFactory, - AdminExportIndex $adminExportIndex + AdminExportIndex $adminExportIndex, + Cron $cron ) { $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; + $this->cron = $cron; } /** @@ -68,12 +79,16 @@ public function test( ExportData $exportData, Customer $customer ) { + $this->cron->run(); + $this->cron->run(); $customer->persist(); $this->adminExportIndex->open(); + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData->persist(); $this->adminExportIndex->getExportForm()->fill($exportData); $this->adminExportIndex->getFilterExport()->clickContinue(); - + $this->cron->run(); + $this->cron->run(); return [ 'customer' => $customer ]; diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml index e2f86d82363c3..ffcafbe687236 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation7" firstConstraint="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" method="test"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">downloadableProduct::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml index ccb74e1f79dc9..e807ec9134277 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/5" xsi:type="string">Downloadable Product</data> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml index f6dcdd5b65d02..4e98019c25317 100644 --- a/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest29"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Email Templates</data> <data name="pageTitle" xsi:type="string">Email Templates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml index cffcdbf45a6dc..a110dc6a89f8c 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation6" summary="Export grouped product with special price" ticketId="MAGETWO-46116"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">groupedProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php index 45481d6ee0758..6c19291222a97 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php @@ -20,6 +20,13 @@ class Grid extends DataGrid */ protected $addProducts = '.action-primary[data-role="action"]'; + /** + * Grid selector. + * + * @var string + */ + private $gridSelector = '[data-role="grid-wrapper"]'; + /** * Filters array mapping * @@ -40,4 +47,59 @@ public function addProducts() { $this->_rootElement->find($this->addProducts)->click(); } + + /** + * @inheritdoc + */ + public function searchAndSelect(array $filter) + { + $this->waitGridVisible(); + $this->waitLoader(); + parent::searchAndSelect($filter); + } + + /** + * @inheritdoc + */ + protected function waitLoader() + { + parent::waitLoader(); + $this->waitGridLoaderInvisible(); + } + + /** + * Wait for grid to appear. + * + * @return void + */ + private function waitGridVisible() + { + $browser = $this->_rootElement; + $selector = $this->gridSelector; + + return $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isVisible() ? true : null; + } + ); + } + + /** + * Wait for grid spinner disappear. + * + * @return void + */ + private function waitGridLoaderInvisible() + { + $browser = $this->_rootElement; + $selector = $this->loader; + + return $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isVisible() === false ? true : null; + } + ); + } } diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php index 5627a9d887bc7..c47df8c5463e5 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php @@ -27,14 +27,15 @@ class View extends ParentView * * @var string */ - protected $formatTierPrice = "//tbody[%row-number%]//ul[contains(@class,'tier')]//*[@class='item'][%line-number%]"; + protected $formatTierPrice = + "//tr[@class='row-tier-price'][%row-number%]//ul[contains(@class,'tier')]//*[@class='item'][%line-number%]"; /** * This member holds the class name of the special price block. * * @var string */ - protected $formatSpecialPrice = '//tbody[%row-number%]//*[contains(@class,"price-box")]'; + protected $formatSpecialPrice = '//tbody//tr[%row-number%]//*[contains(@class,"price-box")]'; /** * Get grouped product block diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml index 38ef02ff49441..f397c1b99e3b2 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\GroupedProduct\Test\TestCase\CreateGroupedProductEntityTest" summary="Create Grouped Product" ticketId="MAGETWO-24877"> <variation name="CreateGroupedProductEntityTestVariation1" summary="Create Grouped Product and Assign It to the Category" ticketId="MAGETWO-13610"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> @@ -57,7 +57,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> </variation> <variation name="CreateGroupedProductEntityTestVariation5" summary="Create with no required products"> - <data name="tag" xsi:type="string">stable:no</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> @@ -110,7 +109,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="CreateGroupedProductEntityTestVariation10" summary="Create Grouped Product and Assign it on Custom Website"> - <data name="tag" xsi:type="string">stable:no</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml index cf0c8c8141678..e62e5ad73958f 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation8" firstConstraint="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" method="test"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">groupedProduct::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml index 5a5cfdd9adc4c..ac57cdeb92053 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/2" xsi:type="string">Grouped Product</data> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Block/Adminhtml/Export/ExportedGrid.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Block/Adminhtml/Export/ExportedGrid.php new file mode 100644 index 0000000000000..60a313a9c01b2 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Block/Adminhtml/Export/ExportedGrid.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ImportExport\Test\Block\Adminhtml\Export; + +use Magento\Ui\Test\Block\Adminhtml\DataGrid; +use Magento\Mtf\Client\Element\SimpleElement; +use Magento\Mtf\Client\Locator; + +/** + * List of exported files + */ +class ExportedGrid extends DataGrid +{ + /** + * Locator value for "Download" link inside action column. + * + * @var string + */ + protected $editLink = '//a[@class="action-menu-item"][text()="Download"]'; + + /** + * First row in the grid selector + * + * @var string + */ + protected $firstRowSelector = '//tr[@data-repeat-index="0"]'; + + /** + * Select action toggle. + * + * @var string + */ + private $selectAction = '.action-select'; + + /** + * Locator value for "Delete" link inside action column. + * + * @var string + */ + private $deleteLink = '//a[@class="action-menu-item"][text()="Delete"]'; + + /** + * Exported grid locator + * + * @var string + */ + private $exportGrid = '.data-grid'; + + /** + * Delete all files from exported grid + */ + public function deleteAllExportedFiles() + { + $this->waifForGrid(); + $firstGridRow = $this->getFirstRow(); + while ($firstGridRow->isVisible()) { + $this->deleteFile($firstGridRow); + } + } + + /** + * Delete exported file from the grid + * + * @param SimpleElement $rowItem + * @return void + */ + private function deleteFile(SimpleElement $rowItem) + { + $rowItem->find($this->selectAction)->click(); + $rowItem->find($this->deleteLink, Locator::SELECTOR_XPATH)->click(); + $this->confirmDeleteModal(); + $this->waitLoader(); + } + + /** + * Get first row from the grid + * + * @return SimpleElement + */ + public function getFirstRow(): SimpleElement + { + return $this->_rootElement->find($this->firstRowSelector, \Magento\Mtf\Client\Locator::SELECTOR_XPATH); + } + + /** + * Download first exported file + * + * @throws \Exception + */ + public function downloadFirstFile() + { + $this->waifForGrid(); + $firstRow = $this->getFirstRow(); + $i = 0; + while (!$firstRow->isVisible()) { + if ($i === 10) { + throw new \Exception('There is no exported file in the grid'); + } + $this->browser->refresh(); + $this->waifForGrid(); + ++$i; + } + $this->clickDownloadLink($firstRow); + } + + /** + * Wait for the grid + * + * @return void + */ + public function waifForGrid() + { + $this->waitForElementVisible($this->exportGrid); + $this->waitLoader(); + } + + /** + * Click on "Download" link. + * + * @param SimpleElement $rowItem + * @return void + */ + private function clickDownloadLink(SimpleElement $rowItem) + { + $rowItem->find($this->selectAction)->click(); + $rowItem->find($this->editLink, Locator::SELECTOR_XPATH)->click(); + } + + /** + * Confirm delete file modal + * + * @return void + */ + private function confirmDeleteModal() + { + $modalElement = $this->browser->find($this->confirmModal); + /** @var \Magento\Ui\Test\Block\Adminhtml\Modal $modal */ + $modal = $this->blockFactory->create( + \Magento\Ui\Test\Block\Adminhtml\Modal::class, + ['element' => $modalElement] + ); + $modal->acceptAlert(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Block/Adminhtml/Export/NotificationsArea.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Block/Adminhtml/Export/NotificationsArea.php new file mode 100644 index 0000000000000..4a781c787eb0e --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Block/Adminhtml/Export/NotificationsArea.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ImportExport\Test\Block\Adminhtml\Export; + +use Magento\Backend\Test\Block\Widget\Grid; +use Magento\Mtf\Client\Locator; + +/** + * Notification messages area + */ +class NotificationsArea extends Grid +{ + /** + * Notifications section drop down locator + * + * @var string + */ + private $notificationsDropdown = '.notifications-action'; + + /** + * First notification description + * + * @var string + */ + private $notificationDescription = '//li[@class="notifications-entry notifications-critical"][1]' + . '/p[@class="notifications-entry-description"]'; + + /** + * Open notifications drop down + * + * @return void + */ + public function openNotificationsDropDown() + { + $this->browser->find($this->notificationsDropdown)->click(); + } + + /** + * Get latest notification message text + * + * @return string + */ + public function getLatestMessage() + { + $this->waitForElementVisible($this->notificationDescription, Locator::SELECTOR_XPATH); + return $this->_rootElement->find($this->notificationDescription, Locator::SELECTOR_XPATH)->getText(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportNoDataErrorMessage.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportNoDataErrorMessage.php index 25e4d4b39174e..c52f8c6613fb7 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportNoDataErrorMessage.php +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportNoDataErrorMessage.php @@ -16,7 +16,7 @@ class AssertExportNoDataErrorMessage extends AbstractConstraint /** * Text value to be checked. */ - const ERROR_MESSAGE = 'There is no data for the export.'; + const ERROR_MESSAGE = 'Error during export process occurred. Please check logs for detail'; /** * Assert that error message is visible after exporting without entity attributes data. @@ -26,7 +26,11 @@ class AssertExportNoDataErrorMessage extends AbstractConstraint */ public function processAssert(AdminExportIndex $adminExportIndex) { - $actualMessage = $adminExportIndex->getMessagesBlock()->getErrorMessage(); + $adminExportIndex->open(); + /** @var \Magento\ImportExport\Test\Block\Adminhtml\Export\NotificationsArea $notificationsArea */ + $notificationsArea = $adminExportIndex->getNotificationsArea(); + $notificationsArea->openNotificationsDropDown(); + $actualMessage = $notificationsArea->getLatestMessage(); \PHPUnit\Framework\Assert::assertEquals( self::ERROR_MESSAGE, diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php new file mode 100644 index 0000000000000..59b1c7570c3de --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertExportSubmittedMessage.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ImportExport\Test\Constraint; + +use Magento\ImportExport\Test\Page\Adminhtml\AdminExportIndex; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert that export submitted message is visible after exporting. + */ +class AssertExportSubmittedMessage extends AbstractConstraint +{ + /** + * Text value to be checked. + */ + const MESSAGE = 'Message is added to queue, wait to get your file soon'; + + /** + * Assert that export submitted message is visible after exporting. + * + * @param AdminExportIndex $adminExportIndex + * @return void + */ + public function processAssert(AdminExportIndex $adminExportIndex) + { + $actualMessage = $adminExportIndex->getMessagesBlock()->getSuccessMessage(); + + \PHPUnit\Framework\Assert::assertEquals( + self::MESSAGE, + $actualMessage, + 'Wrong message is displayed.' + . "\nExpected: " . self::MESSAGE + . "\nActual: " . $actualMessage + ); + } + + /** + * Returns a string representation of the object. + * + * @return string + */ + public function toString() + { + return 'Correct message is displayed.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Page/Adminhtml/AdminExportIndex.xml b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Page/Adminhtml/AdminExportIndex.xml index 51afed2087316..e70a5fc29820c 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Page/Adminhtml/AdminExportIndex.xml +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Page/Adminhtml/AdminExportIndex.xml @@ -9,6 +9,9 @@ <page name="AdminExportIndex" area="Adminhtml" mca="admin/export/index" module="Magento_ImportExport"> <block name="filterExport" class="Magento\ImportExport\Test\Block\Adminhtml\Export\Filter" locator="#export_filter_container" strategy="css selector" /> <block name="exportForm" class="Magento\ImportExport\Test\Block\Adminhtml\Export\Edit\Form" locator="#container" strategy="css selector" /> + <block name="exportedGrid" class="Magento\ImportExport\Test\Block\Adminhtml\Export\ExportedGrid" locator="#container" strategy="css selector" /> <block name="messagesBlock" class="Magento\Backend\Test\Block\Messages" locator="#messages" strategy="css selector" /> + <block name="systemMessageDialog" class="Magento\AdminNotification\Test\Block\System\Messages" locator='.ui-popup-message .modal-inner-wrap' strategy="css selector" /> + <block name="notificationsArea" class="Magento\ImportExport\Test\Block\Adminhtml\Export\NotificationsArea" locator='//ul[contains(@data-mark-as-read-url,"notification")]' strategy="xpath" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml index 6ce4d01d177db..d396a364a3f42 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest35"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Import</data> <data name="pageTitle" xsi:type="string">Import</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest36"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Export</data> <data name="pageTitle" xsi:type="string">Export</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml index 883e9bde47bf8..16ae092e62cad 100644 --- a/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest37"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Index Management</data> <data name="pageTitle" xsi:type="string">Index Management</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml index 22def36751f53..265790ed4b763 100644 --- a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest38"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Integrations</data> <data name="pageTitle" xsi:type="string">Integrations</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php index 3c36fe82b1307..97c43d7c4e2ce 100644 --- a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php @@ -64,6 +64,13 @@ class Navigation extends Block */ private $productQty = '/following-sibling::span[contains(text(), "%s")]'; + /** + * Selector for child element with product quantity. + * + * @var string + */ + private $productQtyInCategory = '/span[contains(text(), "%s")]'; + /** * Remove all applied filters. * @@ -124,10 +131,20 @@ public function applyFilter($filter, $linkPattern) */ public function isCategoryVisible(Category $category, $qty) { - return $this->_rootElement->find( - sprintf($this->categoryName, $category->getName()) . sprintf($this->productQty, $qty), - Locator::SELECTOR_XPATH - )->isVisible(); + $link = sprintf($this->categoryName, $category->getName()); + + if (!$this->_rootElement->find($link, Locator::SELECTOR_XPATH)->isVisible()) { + $this->openFilterContainer('Category', $link); + return $this->_rootElement->find( + $link . sprintf($this->productQtyInCategory, $qty), + Locator::SELECTOR_XPATH + )->isVisible(); + } else { + return $this->_rootElement->find( + $link . sprintf($this->productQty, $qty), + Locator::SELECTOR_XPATH + )->isVisible(); + } } /** diff --git a/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml index 13843a50868e7..b1a6b3e0c4386 100644 --- a/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest46"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Newsletter Template</data> <data name="pageTitle" xsi:type="string">Newsletter Templates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest47"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Newsletter Queue</data> <data name="pageTitle" xsi:type="string">Newsletter Queue</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest48"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Newsletter Subscribers</data> <data name="pageTitle" xsi:type="string">Newsletter Subscribers</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest49"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Newsletter Problem Reports</data> <data name="pageTitle" xsi:type="string">Newsletter Problems Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml b/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml index cbdce59057195..bc529729f1217 100644 --- a/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml +++ b/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\PageCache\Test\TestCase\FlushStaticFilesCacheButtonVisibilityTest" summary="Flush Static Files Cache button visibility" ticketId="MAGETWO-39934"> <variation name="FlushStaticFilesCacheButtonVisibilityTest"> - <data name="tag" xsi:type="string">severity:S3</data> + <data name="tag" xsi:type="string">severity:S3, mftf_migrated:yes</data> <constraint name="Magento\PageCache\Test\Constraint\AssertFlushStaticFilesCacheButtonVisibility" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml index d7d031f559f82..114235e75524f 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml @@ -8,13 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate through admin menu" ticketId="MAGETWO-34874"> <variation name="NavigateMenuTest50"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > PayPal Settlement</data> <data name="pageTitle" xsi:type="string">PayPal Settlement Reports</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest51"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Billing Agreements</data> <data name="pageTitle" xsi:type="string">Billing Agreements</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml index 08cce1ffe7d23..4e3cd1824767a 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml @@ -8,76 +8,91 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate through Reports admin menu"> <variation name="NavigateMenuTest55"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Products in Cart</data> <data name="pageTitle" xsi:type="string">Products in Carts</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest56"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Abandoned Carts</data> <data name="pageTitle" xsi:type="string">Abandoned Carts</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest57"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Orders</data> <data name="pageTitle" xsi:type="string">Orders Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest58"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Tax</data> <data name="pageTitle" xsi:type="string">Tax Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest59"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Invoiced</data> <data name="pageTitle" xsi:type="string">Invoice Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest60"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Coupons</data> <data name="pageTitle" xsi:type="string">Coupons Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest61"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Order Total</data> <data name="pageTitle" xsi:type="string">Order Total Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest62"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Order Count</data> <data name="pageTitle" xsi:type="string">Order Count Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest63"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > New</data> <data name="pageTitle" xsi:type="string">New Accounts Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest64"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Views</data> <data name="pageTitle" xsi:type="string">Product Views Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest65"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Bestsellers</data> <data name="pageTitle" xsi:type="string">Bestsellers Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest66"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Low Stock</data> <data name="pageTitle" xsi:type="string">Low Stock Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest67"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Ordered</data> <data name="pageTitle" xsi:type="string">Ordered Products Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest68"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Downloads</data> <data name="pageTitle" xsi:type="string">Downloads Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest69"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Refresh Statistics</data> <data name="pageTitle" xsi:type="string">Refresh Statistics</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml index 8445a21604cdd..334497cc2f77e 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest70"> - <data name="menuItem" xsi:type="string">Marketing > Reviews</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="menuItem" xsi:type="string">Marketing > All Reviews</data> <data name="pageTitle" xsi:type="string">Reviews</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest71"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > By Customers</data> <data name="pageTitle" xsi:type="string">Customer Reviews Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest72"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > By Products</data> <data name="pageTitle" xsi:type="string">Product Reviews Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest73"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Rating</data> <data name="pageTitle" xsi:type="string">Ratings</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml index e45609bb90c83..fdb396bbbd052 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml @@ -18,7 +18,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderInOrdersGridOnFrontend" /> </variation> <variation name="CancelCreatedOrderTestVariationWithZeroSubtotalCheckout" summary="Cancel order with zero subtotal checkout payment method and check status on storefront"> - <data name="tag" xsi:type="string">stable:no</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/payment_auth_expiration/method" xsi:type="string">free</data> <data name="order/data/shipping_method" xsi:type="string">freeshipping_freeshipping</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml index 38ce04fa56d81..e05d0fea6b129 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml @@ -8,17 +8,20 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\CreateCustomOrderStatusEntityTest" summary="Create Custom Order Status Entity" ticketId="MAGETWO-23412"> <variation name="CreateCustomOrderStatusEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">order_status%isolation%</data> <data name="orderStatus/data/label" xsi:type="string">orderLabel%isolation%</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusInGrid" /> </variation> <variation name="CreateCustomOrderStatusEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">pending</data> <data name="orderStatus/data/label" xsi:type="string">orderLabel%isolation%</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusDuplicateStatus" /> </variation> <variation name="CreateCustomOrderStatusEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">order_status%isolation%</data> <data name="orderStatus/data/label" xsi:type="string">Suspected Fraud</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusSuccessCreateMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml index 3e7b8fad1f04d..880c66483d5f2 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\CreateOrderBackendPartOneTest" summary="Create Order from Admin within Offline Payment Methods" ticketId="MAGETWO-28696"> <variation name="CreateOrderBackendTestVariation1" ticketId="MAGETWO-17063"> - <data name="issue" xsi:type="string">https://github.com/magento-engcom/msi/issues/1624</data> <data name="description" xsi:type="string">Create order with simple product for registered US customer using Fixed shipping method and Cash on Delivery payment method</data> <data name="products/0" xsi:type="string">catalogProductSimple::with_one_custom_option</data> <data name="customer/dataset" xsi:type="string">default</data> @@ -70,7 +69,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderByDateInOrdersGrid" /> </variation> <variation name="CreateOrderBackendTestVariation4"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="description" xsi:type="string">Create order with virtual product for registered UK customer using Bank Transfer payment method</data> <data name="products/0" xsi:type="string">catalogProductVirtual::default</data> <data name="customer/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml index 6ae9d19a898bc..28894ed6cc158 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml @@ -9,7 +9,6 @@ <testCase name="Magento\Ui\Test\TestCase\GridSortingTest" summary="Grid UI Component Sorting" ticketId="MAGETWO-41328"> <variation name="SalesOrderGridSorting"> <data name="tag" xsi:type="string">severity:S2</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="description" xsi:type="string">Verify sales order grid storting</data> <data name="steps" xsi:type="array"> <item name="0" xsi:type="string">-</item> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml index 6f568df8f21ca..8bb4ef56361fb 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml @@ -8,13 +8,11 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\MoveLastOrderedProductsOnOrderPageTest" summary="Add Products to Order from Last Ordered Products Section" ticketId="MAGETWO-27640"> <variation name="MoveLastOrderedProductsOnOrderPageTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/entity_id/products" xsi:type="string">catalogProductSimple::default</data> <constraint name="Magento\Sales\Test\Constraint\AssertProductInItemsOrderedGrid" /> </variation> <variation name="MoveLastOrderedProductsOnOrderPageTestVariation2"> - <data name="issue" xsi:type="string">MAGETWO-58762: Customer grid does not open in MoveLastOrderedProductsOnOrderPageTestVariation2 on Jenkins</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/entity_id/products" xsi:type="string">configurableProduct::configurable_with_qty_1</data> <constraint name="Magento\Sales\Test\Constraint\AssertProductInItemsOrderedGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml index 316ba33f19fdb..5cc673f4b4fa5 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml @@ -8,31 +8,37 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest77"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Orders</data> <data name="pageTitle" xsi:type="string">Orders</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest78"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Invoices</data> <data name="pageTitle" xsi:type="string">Invoices</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest79"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Shipments</data> <data name="pageTitle" xsi:type="string">Shipments</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest80"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Credit Memos</data> <data name="pageTitle" xsi:type="string">Credit Memos</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest81"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Transactions</data> <data name="pageTitle" xsi:type="string">Transactions</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest82"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Order Status</data> <data name="pageTitle" xsi:type="string">Order Status</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php index 2627f99d4c8c2..54cec6cf279f6 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php @@ -28,6 +28,13 @@ class PromoQuoteForm extends FormSections */ protected $waitForSelectorVisible = false; + /** + * Selector of name element on the form. + * + * @var string + */ + private $nameElementSelector = 'input[name=name]'; + /** * Fill form with sections. * @@ -38,6 +45,8 @@ class PromoQuoteForm extends FormSections */ public function fill(FixtureInterface $fixture, SimpleElement $element = null, array $replace = null) { + $this->waitForElementNotVisible($this->waitForSelector); + $this->waitForElementVisible($this->nameElementSelector); $sections = $this->getFixtureFieldsByContainers($fixture); if ($replace) { $sections = $this->prepareData($sections, $replace); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml index 521d7d68ac4a6..5cb5b4db72769 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml @@ -271,7 +271,7 @@ <field name="coupon_type" xsi:type="string">No Coupon</field> <field name="sort_order" xsi:type="string">1</field> <field name="is_rss" xsi:type="string">Yes</field> - <field name="conditions_serialized" xsi:type="string">[Total Items Quantity|equals or greater than|3]{Product attribute combination|FOUND|ALL|:[[Category|is|2]]}</field> + <field name="conditions_serialized" xsi:type="string">[Total Items Quantity|equals or greater than|3]</field> <field name="simple_action" xsi:type="string">Percent of product price discount</field> <field name="discount_amount" xsi:type="string">25</field> <field name="apply_to_shipping" xsi:type="string">No</field> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml index e160fef609545..3dfe4cf118552 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml @@ -31,7 +31,6 @@ <constraint name="Magento\SalesRule\Test\Constraint\AssertCartPriceRuleConditionIsApplied" /> </variation> <variation name="ApplySeveralSalesRuleEntityTestVariation3" summary="Rules with different priority, both are applied"> - <data name="tag" xsi:type="string">stable:no</data> <data name="salesRules/rule1" xsi:type="string">active_sales_rule_product_attribute</data> <data name="salesRules/rule2" xsi:type="string">active_sales_total_items</data> <data name="cartPrice/sub_total" xsi:type="string">250.00</data> @@ -44,7 +43,6 @@ <constraint name="Magento\SalesRule\Test\Constraint\AssertCartPriceRuleConditionIsApplied" /> </variation> <variation name="ApplySeveralSalesRuleEntityTestVariation4" summary="Rules with different priority, none are applied"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="salesRules/rule1" xsi:type="string">active_sales_rule_row_total</data> <data name="salesRules/rule2" xsi:type="string">active_sales_total_items</data> <data name="productForSalesRule1/dataset" xsi:type="string">simple_for_salesrule_1</data> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 586ad2acee203..4995c1feb048e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -75,7 +75,6 @@ <data name="salesRule/data/coupon_code" xsi:type="string">Lorem ipsum dolor sit amet, consectetur adipiscing elit - %isolation%</data> <data name="salesRule/data/simple_action" xsi:type="string">Fixed amount discount for whole cart</data> <data name="salesRule/data/discount_amount" xsi:type="string">60</data> - <data name="salesRule/data/apply_to_shipping" xsi:type="string">No</data> <data name="salesRule/data/simple_free_shipping" xsi:type="string">No</data> <data name="salesRule/data/store_labels/0" xsi:type="string">Coupon code+Fixed amount discount for whole cart</data> <data name="productForSalesRule1/dataset" xsi:type="string">simple_for_salesrule_1</data> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml index 0a100eabcff46..1eeaeaaa483c0 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest83"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Cart Price Rules</data> <data name="pageTitle" xsi:type="string">Cart Price Rules</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml index c4cfe3f7f274c..3bb370a15e977 100644 --- a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml +++ b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml @@ -9,7 +9,7 @@ <testCase name="Magento\Security\Test\TestCase\LockAdminUserWhenCreatingNewUserTest" summary="Lock admin user after entering incorrect password while creating new User"> <variation name="LockAdminUserWhenCreatingNewUserTestVariation1"> <data name="configData" xsi:type="string">user_lockout_failures</data> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="customAdmin/dataset" xsi:type="string">custom_admin_with_default_role</data> <data name="user/data/username" xsi:type="string">AdminUser%isolation%</data> <data name="user/data/firstname" xsi:type="string">FirstName%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml index 0b5b8a059cbbd..b4fbe7bb92929 100644 --- a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml +++ b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Security\Test\TestCase\ResetCustomerPasswordFailedTest" summary="Reset customer password."> <variation name="ResetPasswordTestVariation"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1,mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">customer_US</data> <data name="attempts" xsi:type="string">2</data> <data name="configData" xsi:type="string">captcha_storefront_disable</data> diff --git a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml index 55f1b69504363..f43469358aa9c 100644 --- a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml +++ b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Security\Test\TestCase\ResetUserPasswordFailedTest" summary="Prevent Locked Admin User to Log In into the Backend"> <variation name="ResetUserPasswordTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1,mftf_migrated:yes</data> <data name="customAdmin/dataset" xsi:type="string">custom_admin_with_default_role</data> <data name="attempts" xsi:type="string">2</data> <constraint name="Magento\Security\Test\Constraint\AssertUserPasswordResetFailed" /> diff --git a/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml index cbef8fd52fd80..0f9514ffc7a00 100644 --- a/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest85"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Site Map</data> <data name="pageTitle" xsi:type="string">Site Map</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml index a4abfc5daf29e..4d3677076d303 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml @@ -11,6 +11,7 @@ <data name="configData" xsi:type="string">add_store_code_to_urls</data> <data name="user/dataset" xsi:type="string">default</data> <data name="storeCode" xsi:type="string">default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> <constraint name="Magento\Store\Test\Constraint\AssertStoreCodeInUrl" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml index 8cdeb48bb1c98..dcfe22eb0d5f8 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\CreateStoreGroupEntityTest" summary="Create Store Group" ticketId="MAGETWO-27345"> <variation name="CreateStoreGroupEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">main_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_%isolation%</data> <data name="storeGroup/data/code" xsi:type="string">store_code_%isolation%</data> @@ -18,7 +18,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupOnStoreViewForm" /> </variation> <variation name="CreateStoreGroupEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">custom_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_%isolation%</data> <data name="storeGroup/data/code" xsi:type="string">store_code_%isolation%</data> @@ -29,6 +29,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupOnStoreViewForm" /> </variation> <variation name="CreateStoreGroupEntityTestVariation3" summary="Check the absence of delete button" ticketId="MAGETWO-17475"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="storeGroup/dataset" xsi:type="string">custom_new_group</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupSuccessSaveMessage" /> <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupForm" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml index 5a547f69280e1..e35ef853d1b68 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\CreateWebsiteEntityTest" summary="Create Website" ticketId="MAGETWO-27665"> <variation name="CreateWebsiteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="website/data/name" xsi:type="string">website_%isolation%</data> <data name="website/data/code" xsi:type="string">code_%isolation%</data> <constraint name="Magento\Store\Test\Constraint\AssertWebsiteSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml index 306a9fd2024a4..cd37c555fdb1d 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\DeleteStoreEntityTest" summary="Delete Store View" ticketId="MAGETWO-27942"> <variation name="DeleteStoreEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="store/dataset" xsi:type="string">custom</data> <data name="createBackup" xsi:type="string">Yes</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessDeleteAndBackupMessages" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.xml index 66ae1d8179af5..865530862853a 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\DeleteStoreGroupEntityTest" summary="Delete Store Group" ticketId="MAGETWO-27596"> <variation name="DeleteStoreGroupEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S3</data> + <data name="tag" xsi:type="string">severity:S3, mftf_migrated:yes</data> <data name="storeGroup/dataset" xsi:type="string">custom</data> <data name="createBackup" xsi:type="string">Yes</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupSuccessDeleteAndBackupMessages" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml index 8da208bc3f0f1..ab702dfdd4b78 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\MoveStoreToOtherGroupSameWebsiteTest" summary="Move Store View" ticketId="MAGETWO-58361"> <variation name="MoveStoreTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="storeInitialA/dataset" xsi:type="string">custom_group_custom_store</data> <data name="storeInitialB/dataset" xsi:type="string">custom_group_custom_store</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml index 4a73986673c60..6053352386203 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateStoreEntityTest" summary="Update Store View" ticketId="MAGETWO-27786"> <variation name="UpdateStoreEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="storeInitial/dataset" xsi:type="string">custom</data> <data name="store/data/group_id/dataset" xsi:type="string">default</data> <data name="store/data/name" xsi:type="string">storename_updated%isolation%</data> @@ -21,7 +21,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreFrontend" /> </variation> <variation name="UpdateStoreEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="storeInitial/dataset" xsi:type="string">default</data> <data name="store/data/name" xsi:type="string">storename_updated%isolation%</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml index 593d9c365a92e..9ff67a9ba9fec 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateStoreGroupEntityTest" summary="Update Store Group" ticketId="MAGETWO-27568"> <variation name="UpdateStoreGroupEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="storeGroupOrigin/dataset" xsi:type="string">custom</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">main_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_updated_%isolation%</data> @@ -20,7 +20,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupOnStoreViewForm" /> </variation> <variation name="UpdateStoreGroupEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="storeGroupOrigin/dataset" xsi:type="string">custom</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">custom_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_updated_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml index ac857ad035f44..5db0e7f8baad4 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateWebsiteEntityTest" summary="Update Website" ticketId="MAGETWO-27690"> <variation name="UpdateWebsiteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="websiteOrigin/dataset" xsi:type="string">custom_website</data> <data name="website/data/name" xsi:type="string">website_upd%isolation%</data> <data name="website/data/code" xsi:type="string">code_upd%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml index dc5e121db3d1a..96bfec0121eb7 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Tax\Test\TestCase\DeleteTaxRuleEntityTest" summary="Delete Tax Rule" ticketId="MAGETWO-20924"> <variation name="DeleteTaxRuleEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRule/dataset" xsi:type="string">tax_rule_with_custom_tax_classes</data> <data name="address/data/country_id" xsi:type="string">United States</data> <data name="address/data/region_id" xsi:type="string">California</data> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml index ee225f21462ea..ecf202ddae3a8 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml @@ -8,16 +8,19 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest87"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Tax Rules</data> <data name="pageTitle" xsi:type="string">Tax Rules</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest88"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Tax Zones and Rates</data> <data name="pageTitle" xsi:type="string">Tax Zones and Rates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest89"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Import/Export Tax Rates</data> <data name="pageTitle" xsi:type="string">Import and Export Tax Rates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php index 0f163933d260c..40d3401a207a4 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php @@ -37,7 +37,6 @@ class TaxWithCrossBorderTest extends Injectable { /* tags */ const MVP = 'yes'; - const STABLE = 'no'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml index 71964fc926499..ee38659122301 100644 --- a/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest90"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Themes</data> <data name="pageTitle" xsi:type="string">Themes</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php index 235b0d096533f..56ca47331fa1c 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php @@ -162,6 +162,11 @@ class DataGrid extends Grid */ protected $currentPage = ".//*[@data-ui-id='current-page-input'][not(ancestor::*[@class='sticky-header'])]"; + /** + * Top page element to implement a scrolling in case of grid element not visible. + */ + private $topElementToScroll = 'header.page-header'; + /** * Clear all applied Filters. * @@ -181,7 +186,7 @@ public function resetFilter() * * @return void */ - protected function waitFilterToLoad() + public function waitFilterToLoad() { $this->getTemplateBlock()->waitLoader(); $browser = $this->_rootElement; @@ -368,6 +373,10 @@ public function selectItems(array $items, $isSortable = true) $this->sortGridByField('ID'); } foreach ($items as $item) { + //Scroll to the top of the page in case current page input is not visible. + if (!$this->_rootElement->find($this->currentPage, Locator::SELECTOR_XPATH)->isVisible()) { + $this->browser->find($this->topElementToScroll)->hover(); + } $this->_rootElement->find($this->currentPage, Locator::SELECTOR_XPATH)->setValue(''); $this->waitLoader(); $selectItem = $this->getRow($item)->find($this->selectItem); diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php b/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php index af4ccfdac9d30..0574fc8dc55fc 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php @@ -89,6 +89,7 @@ public function test( $page->open(); /** @var DataGrid $gridBlock */ $gridBlock = $page->$gridRetriever(); + $gridBlock->waitFilterToLoad(); $gridBlock->resetFilter(); $sortingResults = []; diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.xml index 730eb285d6d0c..1f888c3a2a8b9 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CategoryUrlRewriteTest" summary="Check url rewrites in catalog categories after changing url key for store view and moving category." ticketId="MAGETWO-45385"> <variation name="CategoryUrlRewriteTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="storeView/dataset" xsi:type="string">custom</data> <data name="childCategory/dataset" xsi:type="string">default</data> <data name="childCategory/data/category_products/dataset" xsi:type="string">catalogProductSimple::default</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCategoryRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCategoryRewriteEntityTest.xml index 890b1d55a6475..1293c6e89b69a 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCategoryRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCategoryRewriteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CreateCategoryRewriteEntityTest" summary="Create Category URL Rewrites" ticketId="MAGETWO-24280"> <variation name="CreateCategoryRewriteEntityTestVariation1" summary="Add Permanent Redirect for Category" ticketId="MAGETWO-12407"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Category</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">cat%isolation%-redirect.html</data> @@ -18,6 +18,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryRedirect" /> </variation> <variation name="CreateCategoryRewriteEntityTestVariation2" summary="Create Category URL Rewrites with no redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Category</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">test_request%isolation%</data> @@ -27,6 +28,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteInGrid" /> </variation> <variation name="CreateCategoryRewriteEntityTestVariation3" summary="Create Category URL Rewrites with Temporary redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Category</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">request_path%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml index 159663ad391e2..d6c37b851aa12 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CreateCustomUrlRewriteEntityTest" summary="Create Custom URL Rewrites" ticketId="MAGETWO-25474"> <variation name="CreateCustomUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">Custom</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">catalog/category/view/id/%category::default_subcategory%</data> @@ -19,6 +20,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCustomRedirect" /> </variation> <variation name="CreateCustomUrlRewriteEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">Custom</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">catalog/product/view/id/%catalogProductSimple::default%</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductUrlRewriteEntityTest.xml index 1917ec928bfaf..0cf2fd9c0d4e1 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductUrlRewriteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CreateProductUrlRewriteEntityTest" summary="Create Product URL Rewrites" ticketId="MAGETWO-25150"> <variation name="CreateProductUrlRewriteEntityTestVariation1" summary="Add Temporary Redirect for Product" ticketId="MAGETWO-12409"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Product</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> @@ -19,6 +19,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteProductRedirect" /> </variation> <variation name="CreateProductUrlRewriteEntityTestVariation2" summary="Create Product URL Rewrites with no redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Product</data> <data name="product/dataset" xsi:type="string">default</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> @@ -28,6 +29,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteSaveMessage" /> </variation> <variation name="CreateProductUrlRewriteEntityTestVariation3" summary="Create Product URL Rewrites with Temporary redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Product</data> <data name="product/dataset" xsi:type="string">default</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> @@ -38,6 +40,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteProductRedirect" /> </variation> <variation name="CreateProductUrlRewriteEntityTestVariation4" summary="Create Product URL Rewrites with Permanent redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Product</data> <data name="product/dataset" xsi:type="string">default</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> @@ -48,6 +51,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteProductRedirect" /> </variation> <variation name="CreateProductUrlRewriteEntityTestVariation5" summary="Autoupdate URL Rewrites if Subcategories deleted" ticketId="MAGETWO-27325"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">For Product</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductWithSeveralWebsitesUrlRewriteTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductWithSeveralWebsitesUrlRewriteTest.xml index 98e3f9e38382d..8e737f193cf94 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductWithSeveralWebsitesUrlRewriteTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CreateProductWithSeveralWebsitesUrlRewriteTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CreateProductWithSeveralWebsitesUrlRewriteTest" summary="Test product url rewrites when it is created in several websites"> <variation name="CreateSimpleProductEntityWithSeveralWebsites" summary="Create product with several websites and check URL Rewites" ticketId="MAGETWO-27238"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCustomUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCustomUrlRewriteEntityTest.xml index 90f5edec2f46b..8e0e8ebde99fb 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCustomUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\DeleteCustomUrlRewriteEntityTest" summary="Delete Custom URL Rewrites" ticketId="MAGETWO-26337"> <variation name="DeleteCustomUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/dataset" xsi:type="string">default</data> <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteDeletedMessage" /> <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml index cdf1d9e3617d8..30d9e57602af1 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest91"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > URL Rewrites</data> <data name="pageTitle" xsi:type="string">URL Rewrites</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCategoryUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCategoryUrlRewriteEntityTest.xml index 4a88ad01b5a75..b2be1a205212e 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCategoryUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCategoryUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\UpdateCategoryUrlRewriteEntityTest" summary="Update Category URL Rewrites" ticketId="MAGETWO-24838"> <variation name="UpdateCategoryUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="category/dataset" xsi:type="string">default_subcategory</data> <data name="categoryRewrite/dataset" xsi:type="string">default</data> @@ -20,6 +21,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryRedirect" /> </variation> <variation name="UpdateCategoryUrlRewriteEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="category/dataset" xsi:type="string">default_subcategory</data> <data name="categoryRewrite/dataset" xsi:type="string">default</data> @@ -32,6 +34,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryRedirect" /> </variation> <variation name="UpdateCategoryUrlRewriteEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="category/dataset" xsi:type="string">default_subcategory</data> <data name="categoryRewrite/dataset" xsi:type="string">default</data> @@ -44,6 +47,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryRedirect" /> </variation> <variation name="UpdateCategoryUrlRewriteEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="category/dataset" xsi:type="string">default_subcategory</data> <data name="categoryRewrite/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCustomUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCustomUrlRewriteEntityTest.xml index 3671cd767ece9..2405216c12203 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCustomUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\UpdateCustomUrlRewriteEntityTest" summary="Update Custom URL Rewrites" ticketId="MAGETWO-25784"> <variation name="UpdateCustomUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialRewrite/dataset" xsi:type="string">default</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">wishlist/%isolation%</data> @@ -19,7 +20,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteSuccessOutsideRedirect" /> </variation> <variation name="UpdateCustomUrlRewriteEntityTestVariation2"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="initialRewrite/dataset" xsi:type="string">custom_rewrite_wishlist</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">wishlist/%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml index 60de554d594d2..8f12930aa417b 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\UpdateProductUrlRewriteEntityTest" summary="Update Product URL Rewrites" ticketId="MAGETWO-24819"> <variation name="UpdateProductUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">product/%catalogProductSimple::product_100_dollar%</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">test_%isolation%.html</data> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensErrorRevokeMessage.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensErrorRevokeMessage.php deleted file mode 100644 index b1a64c7c7e713..0000000000000 --- a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensErrorRevokeMessage.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\User\Test\Constraint; - -use Magento\User\Test\Page\Adminhtml\UserEdit; -use Magento\Mtf\Constraint\AbstractConstraint; - -/** - * Class AssertAccessTokensErrorRevokeMessage - * Assert that error message appears after click on 'Force Sing-In' button for user without tokens. - */ -class AssertAccessTokensErrorRevokeMessage extends AbstractConstraint -{ - /** - * User revoke tokens error message. - */ - const ERROR_MESSAGE = 'This user has no tokens.'; - - /** - * Assert that error message appears after click on 'Force Sing-In' button for user without tokens. - * - * @param UserEdit $userEdit - * @return void - */ - public function processAssert(UserEdit $userEdit) - { - \PHPUnit\Framework\Assert::assertEquals( - self::ERROR_MESSAGE, - $userEdit->getMessagesBlock()->getErrorMessage() - ); - } - - /** - * Return string representation of object - * - * @return string - */ - public function toString() - { - return self::ERROR_MESSAGE . ' error message is present on UserEdit page.'; - } -} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensSuccessfullyRevoked.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensSuccessfullyRevoked.php new file mode 100644 index 0000000000000..b2e52f6a15a10 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensSuccessfullyRevoked.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\Constraint; + +use Magento\User\Test\Page\Adminhtml\UserEdit; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert that success message appears after click on 'Force Sing-In' button. + */ +class AssertAccessTokensSuccessfullyRevoked extends AbstractConstraint +{ + /** + * User revoke tokens success message. + */ + const SUCCESS_MESSAGE = 'You have revoked the user\'s tokens.'; + + /** + * Assert that success message appears after click on 'Force Sing-In' button. + * + * @param UserEdit $userEdit + * @return void + */ + public function processAssert(UserEdit $userEdit): void + { + \PHPUnit\Framework\Assert::assertEquals( + self::SUCCESS_MESSAGE, + $userEdit->getMessagesBlock()->getSuccessMessage() + ); + } + + /** + * Return string representation of object + * + * @return string + */ + public function toString() + { + return self::SUCCESS_MESSAGE . ' message is present on UserEdit page.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php index f7c56ae1b9653..ecfbc8d353888 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php @@ -10,6 +10,7 @@ use Magento\Mtf\Client\BrowserInterface; use Magento\Mtf\Constraint\AbstractConstraint; use Magento\User\Test\Fixture\User; +use Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep; /** * Asserts that user has only related permissions. @@ -18,6 +19,8 @@ class AssertUserRoleRestrictedAccess extends AbstractConstraint { const DENIED_ACCESS = 'Sorry, you need permissions to view this content.'; + protected $loginStep = 'Magento\User\Test\TestStep\LoginUserOnBackendStep'; + /** * Asserts that user has only related permissions. * @@ -36,7 +39,7 @@ public function processAssert( $denyUrl ) { $this->objectManager->create( - \Magento\User\Test\TestStep\LoginUserOnBackendStep::class, + $this->loginStep, ['user' => $user] )->run(); diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccessWithError.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccessWithError.php new file mode 100644 index 0000000000000..b001893abb4c4 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccessWithError.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Test\Constraint; + +/** + * @inheritdoc + */ +class AssertUserRoleRestrictedAccessWithError extends AssertUserRoleRestrictedAccess +{ + protected $loginStep = 'Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep'; +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php index c0c04628f744d..c4645e6e8916a 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php @@ -7,14 +7,20 @@ namespace Magento\User\Test\Constraint; use Magento\Backend\Test\Page\Adminhtml\Dashboard; -use Magento\User\Test\Fixture\User; use Magento\Mtf\Constraint\AbstractConstraint; +use Magento\User\Test\Fixture\User; +use Magento\User\Test\TestStep\LoginUserOnBackendStep; /** * Verify whether customer has logged in to the Backend. */ class AssertUserSuccessLogin extends AbstractConstraint { + /** + * @var string + */ + protected $loginStep = LoginUserOnBackendStep::class; + /** * Verify whether customer has logged in to the Backend. * @@ -25,7 +31,7 @@ class AssertUserSuccessLogin extends AbstractConstraint public function processAssert(User $user, Dashboard $dashboard) { $this->objectManager->create( - \Magento\User\Test\TestStep\LoginUserOnBackendStep::class, + $this->loginStep, ['user' => $user] )->run(); \PHPUnit\Framework\Assert::assertTrue( diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLoginWithError.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLoginWithError.php new file mode 100644 index 0000000000000..9fed1f4df8573 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLoginWithError.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\Constraint; + +use Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep; + +/** + * Verify whether customer has logged in to the Backend with error alert. + */ +class AssertUserSuccessLoginWithError extends AssertUserSuccessLogin +{ + /** + * @var string + */ + protected $loginStep = LoginUserOnBackendWithErrorStep::class; +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml index d196bebca0e9a..4572738b6cb48 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest52"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Locked Users</data> <data name="pageTitle" xsi:type="string">Locked Users</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest53"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Manage Encryption Key</data> <data name="pageTitle" xsi:type="string">Encryption Key</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest92"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > All Users</data> <data name="pageTitle" xsi:type="string">Users</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest93"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > User Roles</data> <data name="pageTitle" xsi:type="string">Roles</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml index e5fcba9b72c25..afdb72d8c561e 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml @@ -9,7 +9,7 @@ <testCase name="Magento\User\Test\TestCase\RevokeAllAccessTokensForAdminWithoutTokensTest" summary="Revoke All Access Tokens for Admin without Tokens" ticketId="MAGETWO-29675"> <variation name="RevokeAllAccessTokensForAdminWithoutTokensTestVariation1"> <data name="user/dataset" xsi:type="string">custom_admin</data> - <constraint name="Magento\User\Test\Constraint\AssertAccessTokensErrorRevokeMessage" /> + <constraint name="Magento\User\Test\Constraint\AssertAccessTokensSuccessfullyRevoked" /> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml index f7de667cf17ac..a89d1ede80112 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml @@ -32,7 +32,7 @@ <constraint name="Magento\User\Test\Constraint\AssertUserInGrid" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogOut" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> - <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccess" /> + <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccessWithError" /> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php index cc1d0fc980fbf..58450abc71633 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php @@ -121,6 +121,11 @@ public function testUpdateAdminUserRolesEntity( */ public function tearDown() { + sleep(3); + $modalMessage = $this->dashboard->getModalMessage(); + if ($modalMessage->isVisible()) { + $modalMessage->acceptAlert(); + } $this->dashboard->getAdminPanelHeader()->logOut(); } } diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml index 224ccbce10f96..db6a13d0f3551 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml @@ -29,8 +29,8 @@ <constraint name="Magento\User\Test\Constraint\AssertRoleSuccessSaveMessage" /> <constraint name="Magento\User\Test\Constraint\AssertRoleInGrid" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogOut" /> - <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> - <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccess" /> + <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLoginWithError"/> + <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccessWithError" /> </variation> <variation name="UpdateAdminUserRoleEntityTestVariation3"> <data name="user/dataset" xsi:type="string">custom_admin_with_default_role</data> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/CloseErrorAlertStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/CloseErrorAlertStep.php new file mode 100644 index 0000000000000..51d48058c8ae5 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/CloseErrorAlertStep.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\TestStep; + +use Magento\Backend\Test\Page\Adminhtml\Dashboard; +use Magento\Mtf\Client\BrowserInterface; +use Magento\Mtf\TestStep\TestStepInterface; + +/** + * Close access error modal message. + */ +class CloseErrorAlertStep implements TestStepInterface +{ + /** + * @var Dashboard + */ + private $dashboard; + + /** + * @var BrowserInterface + */ + private $browser; + + /** + * @param Dashboard $dashboard + * @param BrowserInterface $browser + */ + public function __construct( + Dashboard $dashboard, + BrowserInterface $browser + ) { + $this->dashboard = $dashboard; + $this->browser = $browser; + } + + /** + * @inheritdoc + */ + public function run() + { + $modalMessage = $this->dashboard->getModalMessage(); + try { + $this->browser->waitUntil( + function () use ($modalMessage) { + return $modalMessage->isVisible() ? true : null; + } + ); + $modalMessage->acceptAlert(); + } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { + //There is no modal to accept. + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php index 4f7e6deed7a85..c244e27d42899 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php @@ -50,7 +50,7 @@ class LoginUserOnBackendStep implements TestStepInterface * * @var BrowserInterface */ - private $browser; + protected $browser; /** * Array of error messages on admin login form. @@ -108,8 +108,6 @@ public function run() } } } - - $this->dashboard->getSystemMessageDialog()->closePopup(); } /** diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendWithErrorStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendWithErrorStep.php new file mode 100644 index 0000000000000..094f90d0a5d70 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendWithErrorStep.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\TestStep; + +use Magento\Backend\Test\Page\AdminAuthLogin; +use Magento\Backend\Test\Page\Adminhtml\Dashboard; +use Magento\Mtf\Client\BrowserInterface; +use Magento\User\Test\Fixture\User; + +/** + * Login user on backend with access error. + */ +class LoginUserOnBackendWithErrorStep extends LoginUserOnBackendStep +{ + /** + * @var CloseErrorAlertStep + */ + private $closeErrorAlertStep; + + /** + * @param LogoutUserOnBackendStep $logoutUserOnBackendStep + * @param AdminAuthLogin $adminAuth + * @param User $user + * @param Dashboard $dashboard + * @param BrowserInterface $browser + */ + public function __construct( + LogoutUserOnBackendStep $logoutUserOnBackendStep, + AdminAuthLogin $adminAuth, + User $user, + Dashboard $dashboard, + BrowserInterface $browser, + CloseErrorAlertStep $closeErrorAlertStep + ) { + parent::__construct($logoutUserOnBackendStep, $adminAuth, $user, $dashboard, $browser); + $this->closeErrorAlertStep = $closeErrorAlertStep; + } + + /** + * Run step flow. + * + * @return void + */ + public function run() + { + parent::run(); + $this->closeErrorAlertStep->run(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php index 70a4080a0b4d5..7f366312bba24 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php @@ -48,7 +48,6 @@ public function __construct(AdminAuthLogin $adminAuth, Dashboard $dashboard) public function run() { $this->adminAuth->open(); - $this->dashboard->getSystemMessageDialog()->closePopup(); $this->dashboard->getAdminPanelHeader()->logOut(); } } diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendWithErrorStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendWithErrorStep.php new file mode 100644 index 0000000000000..ce49e86afc065 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendWithErrorStep.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\TestStep; + +use Magento\Backend\Test\Page\AdminAuthLogin; +use Magento\Backend\Test\Page\Adminhtml\Dashboard; + +/** + * Logout user on backend with access error. + */ +class LogoutUserOnBackendWithErrorStep extends LogoutUserOnBackendStep +{ + /** + * @var CloseErrorAlertStep + */ + private $closeErrorAlertStep; + + public function __construct( + AdminAuthLogin $adminAuth, + Dashboard $dashboard, + CloseErrorAlertStep $closeErrorAlertStep + ) { + parent::__construct($adminAuth, $dashboard); + $this->closeErrorAlertStep = $closeErrorAlertStep; + } + + /** + * @inheritdoc + */ + public function run() + { + $this->adminAuth->open(); + $this->closeErrorAlertStep->run(); + $this->dashboard->getAdminPanelHeader()->logOut(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/User/Test/etc/di.xml new file mode 100644 index 0000000000000..1298bd56a8fb0 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/etc/di.xml @@ -0,0 +1,14 @@ +<?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\User\Test\TestStep\LoginUserOnBackendWithErrorStep"> + <arguments> + <argument name="logoutUserOnBackendStep" xsi:type="object">\Magento\User\Test\TestStep\LogoutUserOnBackendWithErrorStep</argument> + </arguments> + </type> +</config> diff --git a/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml index 3fceddf1a807a..0a9c74cd92bc6 100644 --- a/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest94"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Custom Variables</data> <data name="pageTitle" xsi:type="string">Custom Variables</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php b/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php index 1a024eefe162d..13c16c888fbb0 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php @@ -172,7 +172,7 @@ protected function prepareWidgetInstance(array $data) $widgetInstances = []; foreach ($data['widget_instance'] as $key => $widgetInstance) { $pageGroup = $widgetInstance['page_group']; - $method = 'prepare' . str_replace(' ', '', ucwords(str_replace('_', ' ', $pageGroup))) . 'Group'; + $method = 'prepare' . str_replace('_', '', ucwords($pageGroup, '_')) . 'Group'; if (!method_exists(__CLASS__, $method)) { throw new \Exception('Method for prepare page group "' . $method . '" is not exist.'); } diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml index 6b3215dd30d16..6a2a533c59df2 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest96"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Widgets</data> <data name="pageTitle" xsi:type="string">Widgets</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Block/Customer/Wishlist/Items/TopToolbar.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Block/Customer/Wishlist/Items/TopToolbar.php new file mode 100644 index 0000000000000..9d2d0fee46b53 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Block/Customer/Wishlist/Items/TopToolbar.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Block\Customer\Wishlist\Items; + +use Magento\Mtf\Block\Block; +use Magento\Mtf\Client\Locator; + +/** + * Pager block for wishlist items page. + */ +class TopToolbar extends Block +{ + /** + * Selector next active element + * + * @var string + */ + private $nextPageSelector = '.item.current + .item a'; + + /** + * Selector first element + * + * @var string + */ + private $firstPageSelector = '.item>.page'; + + /** + * Selector option element + * + * @var string + */ + private $optionSelector = './/option'; + + /** + * Go to the next page + * + * @return bool + */ + public function nextPage() + { + $nextPageItem = $this->_rootElement->find($this->nextPageSelector); + if ($nextPageItem->isVisible()) { + $nextPageItem->click(); + return true; + } + return false; + } + + /** + * Go to the first page + * + * @return bool + */ + public function firstPage() + { + $firstPageItem = $this->_rootElement->find($this->firstPageSelector); + if ($firstPageItem->isVisible()) { + $firstPageItem->click(); + return true; + } + return false; + } + + /** + * Set value for limiter element by index + * + * @param int $index + * @return $this + */ + public function setLimiterValueByIndex($index) + { + $options = $this->_rootElement->getElements($this->optionSelector, Locator::SELECTOR_XPATH); + if (isset($options[$index])) { + $options[$index]->click(); + } + return $this; + } + + /** + * Get value for limiter element by index + * + * @param int $index + * @return int|null + */ + public function getLimitedValueByIndex($index) + { + $options = $this->_rootElement->getElements($this->optionSelector, Locator::SELECTOR_XPATH); + if (isset($options[$index])) { + return $options[$index]->getValue(); + } + return null; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php index ae994f84c47f7..058c764be16a4 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php @@ -36,6 +36,13 @@ public function processAssert( $cmsIndex->getLinksBlock()->openLink('My Account'); $customerAccountIndex->getAccountMenuBlock()->openMenuItem('My Wish List'); + $isProductVisible = $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product) + ->isVisible(); + while (!$isProductVisible && $wishlistIndex->getTopToolbar()->nextPage()) { + $isProductVisible = $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product) + ->isVisible(); + } + \PHPUnit\Framework\Assert::assertTrue( $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product)->isVisible(), $product->getName() . ' is not visible on Wish List page.' diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php index 68e30e13558ca..dc71939d4790d 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php @@ -44,44 +44,40 @@ public function processAssert( $cmsIndex->getLinksBlock()->openLink('My Account'); $customerAccountIndex->getAccountMenuBlock()->openMenuItem('My Wish List'); - $productRegularPrice = 0; - if ($product instanceof GroupedProduct) { - $associatedProducts = $product->getAssociated(); + $isProductVisible = $wishlistIndex->getWishlistBlock() + ->getProductItemsBlock() + ->getItemProduct($product) + ->isVisible(); + while (!$isProductVisible && $wishlistIndex->getTopToolbar()->nextPage()) { + $isProductVisible = $wishlistIndex->getWishlistBlock() + ->getProductItemsBlock() + ->getItemProduct($product) + ->isVisible(); + } - /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $associatedProduct */ - foreach ($associatedProducts['products'] as $key => $associatedProduct) { - $qty = $associatedProducts['assigned_products'][$key]['qty']; - $price = $associatedProduct->getPrice(); - $productRegularPrice += $qty * $price; - } + if ($product instanceof GroupedProduct) { + $productRegularPrice = $this->getGroupedProductRegularPrice($product); } elseif ($product instanceof BundleProduct) { - $bundleSelection = (array)$product->getBundleSelections(); - foreach ($bundleSelection['products'] as $bundleOption) { - $regularBundleProductPrice = 0; - /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $bundleProduct */ - foreach ($bundleOption as $bundleProduct) { - $checkoutData = $bundleProduct->getCheckoutData(); - $bundleProductPrice = $checkoutData['qty'] * $checkoutData['cartItem']['price']; - if (0 === $regularBundleProductPrice) { - $regularBundleProductPrice = $bundleProductPrice; - } else { - $regularBundleProductPrice = max([$bundleProductPrice, $regularBundleProductPrice]); - } - } - $productRegularPrice += $regularBundleProductPrice; - } + $productRegularPrice = $this->getBundleProductRegularPrice($product); } else { - $productRegularPrice = (float)$product->getPrice(); + $productRegularPrice = (float) $product->getPrice(); } - $productItem = $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product); - $wishListProductRegularPrice = (float)$productItem->getRegularPrice(); + $productItem = $wishlistIndex->getWishlistBlock() + ->getProductItemsBlock() + ->getItemProduct($product); - \PHPUnit\Framework\Assert::assertEquals( - $this->regularPriceLabel, - $productItem->getPriceLabel(), - 'Wrong product regular price is displayed.' - ); + $wishListProductRegularPrice = $product instanceof BundleProduct + ? (float)$productItem->getPrice() + : (float)$productItem->getRegularPrice(); + + if (!$product instanceof BundleProduct) { + \PHPUnit\Framework\Assert::assertEquals( + $this->regularPriceLabel, + $productItem->getPriceLabel(), + 'Wrong product regular price is displayed.' + ); + } \PHPUnit\Framework\Assert::assertNotEmpty( $wishListProductRegularPrice, @@ -95,6 +91,52 @@ public function processAssert( ); } + /** + * Retrieve grouped product regular price + * + * @param GroupedProduct $product + * @return float + */ + private function getGroupedProductRegularPrice(GroupedProduct $product) + { + $productRegularPrice = 0; + $associatedProducts = $product->getAssociated(); + /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $associatedProduct */ + foreach ($associatedProducts['products'] as $key => $associatedProduct) { + $qty = $associatedProducts['assigned_products'][$key]['qty']; + $price = $associatedProduct->getPrice(); + $productRegularPrice += $qty * $price; + } + return $productRegularPrice; + } + + /** + * Retrieve bundle product regular price + * + * @param BundleProduct $product + * @return float + */ + private function getBundleProductRegularPrice(BundleProduct $product) + { + $productRegularPrice = 0; + $bundleSelection = (array) $product->getBundleSelections(); + foreach ($bundleSelection['products'] as $bundleOption) { + $regularBundleProductPrice = 0; + /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $bundleProduct */ + foreach ($bundleOption as $bundleProduct) { + $checkoutData = $bundleProduct->getCheckoutData(); + $bundleProductPrice = $checkoutData['qty'] * $checkoutData['cartItem']['price']; + if (0 === $regularBundleProductPrice) { + $regularBundleProductPrice = $bundleProductPrice; + } else { + $regularBundleProductPrice = max([$bundleProductPrice, $regularBundleProductPrice]); + } + } + $productRegularPrice += $regularBundleProductPrice; + } + return $productRegularPrice; + } + /** * Returns a string representation of the object. * diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml index 141bc8c5898c2..4e67c8d4e1dd4 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml @@ -9,5 +9,6 @@ <page name="WishlistIndex" mca="wishlist/index/index" module="Magento_Wishlist"> <block name="messagesBlock" class="Magento\Backend\Test\Block\Messages" locator=".messages" strategy="css selector"/> <block name="wishlistBlock" class="Magento\Wishlist\Test\Block\Customer\Wishlist" locator="#wishlist-view-form" strategy="css selector"/> + <block name="topToolbar" class="Magento\Wishlist\Test\Block\Customer\Wishlist\Items\TopToolbar" locator=".//*[contains(@class,'wishlist-toolbar')][2]" strategy="xpath"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml index 06b1a6078d5c7..e5fa4b6fc11ee 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml @@ -108,7 +108,7 @@ </variation> <variation name="AddProductToWishlistEntityTestVariation14" ticketId="MAGETWO-90131"> <data name="product" xsi:type="array"> - <item name="0" xsi:type="string">bundleProduct::with_special_price_and_custom_options</item> + <item name="0" xsi:type="string">bundleProduct::default_with_one_simple_product</item> </data> <constraint name="Magento\Wishlist\Test\Constraint\AssertAddProductToWishlistSuccessMessage"/> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist"/> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ShareWishlistEntityTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ShareWishlistEntityTest.xml index ec01f57202b26..cbf5ba392844e 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ShareWishlistEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ShareWishlistEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Wishlist\Test\TestCase\ShareWishlistEntityTest" summary="Share wishlist" ticketId="MAGETWO-23394"> <variation name="ShareWishlistEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="sharingInfo/emails" xsi:type="string">JohnDoe123456789@example.com,JohnDoe987654321@example.com,JohnDoe123456abc@example.com</data> <data name="sharingInfo/message" xsi:type="string">Sharing message.</data> <constraint name="Magento\Wishlist\Test\Constraint\AssertWishlistShareMessage" /> diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index 8eaf82475a4e4..99025dd1cffcc 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.IncludeFile require_once __DIR__ . '/../../../../app/bootstrap.php'; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; +// phpcs:ignore Magento2.Security.Superglobal if (isset($_GET['command'])) { + // phpcs:ignore Magento2.Security.Superglobal $command = urldecode($_GET['command']); + // phpcs:ignore Magento2.Security.Superglobal $magentoObjectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, $_SERVER); + // phpcs:ignore Magento2.Security.Superglobal $magentoObjectManager = $magentoObjectManagerFactory->create($_SERVER); $cli = $magentoObjectManager->create(\Magento\Framework\Console\Cli::class); $input = new StringInput($command); diff --git a/dev/tests/functional/utils/deleteMagentoGeneratedCode.php b/dev/tests/functional/utils/deleteMagentoGeneratedCode.php index 99aa9af06e92a..17260bd1da635 100644 --- a/dev/tests/functional/utils/deleteMagentoGeneratedCode.php +++ b/dev/tests/functional/utils/deleteMagentoGeneratedCode.php @@ -4,4 +4,5 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.InsecureFunction exec('rm -rf ../../../../generated/*'); diff --git a/dev/tests/functional/utils/export.php b/dev/tests/functional/utils/export.php index 343dcc557c832..fa50bc729d0f6 100644 --- a/dev/tests/functional/utils/export.php +++ b/dev/tests/functional/utils/export.php @@ -4,12 +4,16 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (!isset($_GET['template'])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \InvalidArgumentException('Argument "template" must be set.'); } -$varDir = '../../../../var/'; +$varDir = '../../../../var/export/'; +// phpcs:ignore Magento2.Security.Superglobal $template = urldecode($_GET['template']); +// phpcs:ignore Magento2.Functions.DiscouragedFunction $fileList = scandir($varDir, SCANDIR_SORT_NONE); $files = []; @@ -17,11 +21,14 @@ if (preg_match("`$template`", $fileName) === 1) { $filePath = $varDir . $fileName; $files[] = [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'content' => file_get_contents($filePath), 'name' => $fileName, + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'date' => filectime($filePath), ]; } } +// phpcs:ignore Magento2.Security.LanguageConstruct, Magento2.Security.InsecureFunction echo serialize($files); diff --git a/dev/tests/functional/utils/locales.php b/dev/tests/functional/utils/locales.php index 827b8b1b89448..11e1e2b70fa50 100644 --- a/dev/tests/functional/utils/locales.php +++ b/dev/tests/functional/utils/locales.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (isset($_GET['type']) && $_GET['type'] == 'deployed') { + // phpcs:ignore Magento2.Security.Superglobal $themePath = isset($_GET['theme_path']) ? $_GET['theme_path'] : 'adminhtml/Magento/backend'; $directory = __DIR__ . '/../../../../pub/static/' . $themePath; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $locales = array_diff(scandir($directory), ['..', '.']); } else { + // phpcs:ignore Magento2.Security.IncludeFile require_once __DIR__ . DIRECTORY_SEPARATOR . 'bootstrap.php'; $localeConfig = $magentoObjectManager->create(\Magento\Framework\Locale\Config::class); $locales = $localeConfig->getAllowedLocales(); } +// phpcs:ignore Magento2.Security.LanguageConstruct echo implode('|', $locales); diff --git a/dev/tests/functional/utils/log.php b/dev/tests/functional/utils/log.php index 68a68d4bad648..30783ae8e1d28 100644 --- a/dev/tests/functional/utils/log.php +++ b/dev/tests/functional/utils/log.php @@ -5,15 +5,19 @@ */ declare(strict_types=1); - +// phpcs:ignore Magento2.Security.Superglobal if (!isset($_GET['name'])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \InvalidArgumentException( 'The name of log file is required for getting logs.' ); } + +// phpcs:ignore Magento2.Security.Superglobal $name = urldecode($_GET['name']); if (preg_match('/\.\.(\\\|\/)/', $name)) { throw new \InvalidArgumentException('Invalid log file name'); } +// phpcs:ignore Magento2.Security.InsecureFunction, Magento2.Functions.DiscouragedFunction, Magento2.Security.LanguageConstruct echo serialize(file_get_contents('../../../../var/log' .'/' .$name)); diff --git a/dev/tests/functional/utils/pathChecker.php b/dev/tests/functional/utils/pathChecker.php index 11f8229bce56f..217cf90af0a56 100644 --- a/dev/tests/functional/utils/pathChecker.php +++ b/dev/tests/functional/utils/pathChecker.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (isset($_GET['path'])) { + // phpcs:ignore Magento2.Security.Superglobal $path = urldecode($_GET['path']); - + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (file_exists('../../../../' . $path)) { + // phpcs:ignore Magento2.Security.LanguageConstruct echo 'path exists: true'; } else { + // phpcs:ignore Magento2.Security.LanguageConstruct echo 'path exists: false'; } } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \InvalidArgumentException("GET parameter 'path' is not set."); } diff --git a/dev/tests/functional/utils/website.php b/dev/tests/functional/utils/website.php index 625f5c6b483f8..720b4962aedd4 100644 --- a/dev/tests/functional/utils/website.php +++ b/dev/tests/functional/utils/website.php @@ -4,13 +4,17 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (!isset($_GET['website_code'])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("website_code GET parameter is not set."); } +// phpcs:ignore Magento2.Security.Superglobal $websiteCode = urldecode($_GET['website_code']); $rootDir = '../../../../'; $websiteDir = $rootDir . 'websites/' . $websiteCode . '/'; +// phpcs:ignore Magento2.Functions.DiscouragedFunction $contents = file_get_contents($rootDir . 'index.php'); $websiteParam = <<<EOD @@ -25,8 +29,10 @@ $contents = preg_replace($pattern, $replacement, $contents); $old = umask(0); +// phpcs:ignore Magento2.Functions.DiscouragedFunction mkdir($websiteDir, 0760, true); umask($old); - +// phpcs:ignore Magento2.Functions.DiscouragedFunction copy($rootDir . '.htaccess', $websiteDir . '.htaccess'); +// phpcs:ignore Magento2.Functions.DiscouragedFunction file_put_contents($websiteDir . 'index.php', $contents); diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObject.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObject.php new file mode 100644 index 0000000000000..ad3033cf7eeaa --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObject.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestModuleMysqlMq\Model; + +class DataObject extends \Magento\Framework\Api\AbstractExtensibleObject +{ + /** + * @return string + */ + public function getName() + { + return $this->_get('name'); + } + + /** + * @param string $name + * @return $this + */ + public function setName($name) + { + return $this->setData('name', $name); + } + + /** + * @return int|null + */ + public function getEntityId() + { + return $this->_get('entity_id'); + } + + /** + * @param int $entityId + * @return $this + */ + public function setEntityId($entityId) + { + return $this->setData('entity_id', $entityId); + } + + /** + * @return string + */ + public function getOutputPath() + { + return $this->_get('outputPath'); + } + + /** + * @param string $path + * @return $this + */ + public function setOutputPath($path) + { + return $this->setData('outputPath', $path); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObjectRepository.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObjectRepository.php new file mode 100644 index 0000000000000..942298e49972f --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObjectRepository.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestModuleMysqlMq\Model; + +class DataObjectRepository +{ + /** + * @param DataObject $dataObject + * @param string $requiredParam + * @param int|null $optionalParam + * @return null + */ + public function delayedOperation( + \Magento\TestModuleMysqlMq\Model\DataObject $dataObject, + $requiredParam, + $optionalParam = null + ) { + $output = "Processed '{$dataObject->getEntityId()}'; " + . "Required param '{$requiredParam}'; Optional param '{$optionalParam}'\n"; + file_put_contents($dataObject->getOutputPath(), $output); + + return null; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/Processor.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/Processor.php new file mode 100644 index 0000000000000..fb6fd4c5c2802 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/Processor.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestModuleMysqlMq\Model; + +/** + * Test message processor is used by \Magento\MysqlMq\Model\PublisherConsumerTest + */ +class Processor +{ + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processMessage($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processObjectCreated($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed object created {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processCustomObjectCreated($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed custom object created {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processObjectUpdated($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed object updated {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processMessageWithException($message) + { + file_put_contents($message->getOutputPath(), "Exception processing {$message->getEntityId()}"); + throw new \LogicException( + "Exception during message processing happened. Entity: {{$message->getEntityId()}}" + ); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/communication.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/communication.xml new file mode 100644 index 0000000000000..4d6269dbb7920 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/communication.xml @@ -0,0 +1,14 @@ +<?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:Communication/etc/communication.xsd"> + <topic name="demo.exception" request="Magento\TestModuleMysqlMq\Model\DataObject"/> + <topic name="test.schema.defined.by.method" schema="Magento\TestModuleMysqlMq\Model\DataObjectRepository::delayedOperation" is_synchronous="false"/> + <topic name="demo.object.created" request="Magento\TestModuleMysqlMq\Model\DataObject"/> + <topic name="demo.object.updated" request="Magento\TestModuleMysqlMq\Model\DataObject"/> + <topic name="demo.object.custom.created" request="Magento\TestModuleMysqlMq\Model\DataObject"/> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/module.xml new file mode 100644 index 0000000000000..8b6ea0f44ce9c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/module.xml @@ -0,0 +1,11 @@ +<?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_TestModuleMysqlMq" active="true"> + </module> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue.xml new file mode 100644 index 0000000000000..362237c0c5e62 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue.xml @@ -0,0 +1,24 @@ +<?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-message-queue:etc/queue.xsd"> + <broker topic="demo.exception" type="db" exchange="magento"> + <queue consumer="demoConsumerWithException" name="queue-exception" handler="Magento\TestModuleMysqlMq\Model\Processor::processMessageWithException"/> + </broker> + <broker topic="test.schema.defined.by.method" type="db" exchange="magento"> + <queue consumer="delayedOperationConsumer" name="demo-queue-6" handler="Magento\TestModuleMysqlMq\Model\DataObjectRepository::delayedOperation"/> + </broker> + <broker topic="demo.object.created" type="db" exchange="magento"> + <queue consumer="demoConsumerQueueOne" name="queue-created" handler="\Magento\TestModuleMysqlMq\Model\Processor::processObjectCreated"/> + </broker> + <broker topic="demo.object.updated" exchange="magento" type="db"> + <queue consumer="demoConsumerQueueTwo" name="queue-updated" handler="\Magento\TestModuleMysqlMq\Model\Processor::processObjectUpdated"/> + </broker> + <broker topic="demo.object.custom.created" exchange="magento" type="db"> + <queue consumer="demoConsumerQueueThree" name="queue-custom-created" handler="\Magento\TestModuleMysqlMq\Model\Processor::processCustomObjectCreated"/> + </broker> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_consumer.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_consumer.xml new file mode 100644 index 0000000000000..bb495a123a05d --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_consumer.xml @@ -0,0 +1,14 @@ +<?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-message-queue:etc/consumer.xsd"> + <consumer name="demoConsumerQueueOne" queue="queue-created" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processObjectCreated"/> + <consumer name="demoConsumerQueueTwo" queue="queue-updated" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processObjectUpdated"/> + <consumer name="demoConsumerQueueThree" queue="queue-custom-created" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processCustomObjectCreated"/> + <consumer name="demoConsumerWithException" queue="queue-exception" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processMessageWithException"/> + <consumer name="delayedOperationConsumer" queue="demo-queue-6" connection="db" handler="Magento\TestModuleMysqlMq\Model\DataObjectRepository::delayedOperation"/> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_publisher.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_publisher.xml new file mode 100644 index 0000000000000..a665e10ef5f14 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_publisher.xml @@ -0,0 +1,24 @@ +<?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-message-queue:etc/publisher.xsd"> + <publisher topic="demo.exception"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="test.schema.defined.by.method"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="demo.object.created"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="demo.object.updated"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="demo.object.custom.created"> + <connection name="db" exchange="magento"/> + </publisher> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_topology.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_topology.xml new file mode 100644 index 0000000000000..2df5485ee3447 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_topology.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-message-queue:etc/topology.xsd"> + + <exchange name="magento" type="topic" connection="db"> + <binding id="demo.exception.consumer" topic="demo.exception" destination="queue-exception" destinationType="queue"/> + <binding id="test.schema.defined.by.method" topic="test.schema.defined.by.method" destination="demo-queue-6" destinationType="queue"/> + <binding id="demo.object.created" topic="demo.object.created" destination="queue-created" destinationType="queue"/> + <binding id="demo.object.updated" topic="demo.object.updated" destination="queue-updated" destinationType="queue"/> + <binding id="demo.object.all" topic="demo.object.*" destination="queue-custom-created" destinationType="queue"/> + </exchange> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/registration.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/registration.php new file mode 100644 index 0000000000000..4250e95bd7cc3 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/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_TestModuleMysqlMq') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleMysqlMq', __DIR__); +} diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index b871fe1905910..3065b1712640b 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -27,4 +27,7 @@ \Magento\Framework\App\Config\ScopeConfigInterface::class => \Magento\TestFramework\App\Config::class, \Magento\Framework\App\ResourceConnection\ConfigInterface::class => \Magento\Framework\App\ResourceConnection\Config::class, + \Magento\Framework\Lock\Backend\Cache::class => + \Magento\TestFramework\Lock\Backend\DummyLocker::class, + \Magento\Framework\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::class, ]; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php index dc525a46428c4..ddebbf37b16d1 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php @@ -4,13 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Implementation of the @magentoDataFixture DocBlock annotation - */ namespace Magento\TestFramework\Annotation; +use Magento\Framework\Component\ComponentRegistrar; use PHPUnit\Framework\Exception; +/** + * Implementation of the @magentoDataFixture DocBlock annotation. + */ class DataFixture { /** @@ -126,6 +127,8 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null $fixtureMethod = [get_class($test), $fixture]; if (is_callable($fixtureMethod)) { $result[] = $fixtureMethod; + } elseif ($this->isModuleAnnotation($fixture)) { + $result[] = $this->getModulePath($fixture); } else { $result[] = $this->_fixtureBaseDir . '/' . $fixture; } @@ -135,6 +138,44 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null } /** + * Check is the Annotation like Magento_InventoryApi::Test/_files/products.php + * + * @param string $fixture + * @return bool + */ + private function isModuleAnnotation(string $fixture) + { + return (strpos($fixture, '::') !== false); + } + + /** + * Resolve the Fixture + * + * @param string $fixture + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getModulePath(string $fixture) + { + [$moduleName, $fixtureFile] = explode('::', $fixture, 2); + + $modulePath = (new ComponentRegistrar())->getPath(ComponentRegistrar::MODULE, $moduleName); + + if ($modulePath === null) { + throw new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase('Can\'t find registered Module with name %1 .', [$moduleName]) + ); + } + + return $modulePath . '/' . ltrim($fixtureFile, '/'); + } + + /** + * Get method annotations. + * + * Overwrites class-defined annotations. + * * @param \PHPUnit\Framework\TestCase $test * @return array */ diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php index db7f57362d807..ba23ecd13b6a7 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php @@ -4,13 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Implementation of the @magentoDataFixture DocBlock annotation - */ namespace Magento\TestFramework\Annotation; +use Magento\Framework\Component\ComponentRegistrar; use PHPUnit\Framework\Exception; +/** + * Implementation of the @magentoDataFixtureBeforeTransaction DocBlock annotation + */ class DataFixtureBeforeTransaction { /** @@ -93,6 +94,8 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null $fixtureMethod = [get_class($test), $fixture]; if (is_callable($fixtureMethod)) { $result[] = $fixtureMethod; + } elseif ($this->isModuleAnnotation($fixture)) { + $result[] = $this->getModulePath($fixture); } else { $result[] = $this->_fixtureBaseDir . '/' . $fixture; } @@ -102,6 +105,42 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null } /** + * Check is the Annotation like Magento_InventoryApi::Test/_files/products.php + * + * @param string $fixture + * @return bool + */ + private function isModuleAnnotation(string $fixture) + { + return (strpos($fixture, '::') !== false); + } + + /** + * Resolve the Fixture + * + * @param string $fixture + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getModulePath(string $fixture) + { + [$moduleName, $fixtureFile] = explode('::', $fixture, 2); + + $modulePath = (new ComponentRegistrar())->getPath(ComponentRegistrar::MODULE, $moduleName); + + if ($modulePath === null) { + throw new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase('Can\'t find registered Module with name %1 .', [$moduleName]) + ); + } + + return $modulePath . '/' . ltrim($fixtureFile, '/'); + } + + /** + * Get annotations for test. + * * @param \PHPUnit\Framework\TestCase $test * @return array */ diff --git a/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php b/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php index 992b980d6a80d..aa0c790eeac89 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php @@ -13,26 +13,62 @@ */ class Amqp { + const CONFIG_PATH_HOST = 'queue/amqp/host'; + const CONFIG_PATH_USER = 'queue/amqp/user'; + const CONFIG_PATH_PASSWORD = 'queue/amqp/password'; + const DEFAULT_MANAGEMENT_PORT = '15672'; + /** * @var Curl */ private $curl; + /** + * @var \Magento\Framework\App\DeploymentConfig + */ + private $deploymentConfig; + /** * RabbitMQ API host * * @var string */ - private $host = 'http://localhost:15672/api/'; + private $host; /** * Initialize dependencies. + * @param \Magento\Framework\App\DeploymentConfig $deploymentConfig */ - public function __construct() - { + public function __construct( + \Magento\Framework\App\DeploymentConfig $deploymentConfig = null + ) { + $this->deploymentConfig = $deploymentConfig ?? \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Framework\App\DeploymentConfig::class); $this->curl = new Curl(); - $this->curl->setCredentials('guest', 'guest'); + $this->curl->setCredentials( + $this->deploymentConfig->get(self::CONFIG_PATH_USER), + $this->deploymentConfig->get(self::CONFIG_PATH_PASSWORD) + ); $this->curl->addHeader('content-type', 'application/json'); + $this->host = sprintf( + 'http://%s:%s/api/', + $this->deploymentConfig->get(self::CONFIG_PATH_HOST), + defined('RABBITMQ_MANAGEMENT_PORT') ? RABBITMQ_MANAGEMENT_PORT : self::DEFAULT_MANAGEMENT_PORT + ); + } + + /** + * Check that the RabbitMQ instance has the management plugin installed and the api is available. + * + * @return bool + */ + public function isAvailable(): bool + { + $this->curl->get($this->host . 'overview'); + $data = $this->curl->getBody(); + $data = json_decode($data, true); + + return isset($data['management_version']); } /** @@ -55,6 +91,7 @@ public function getExchanges() /** * Get declared exchange bindings. * + * @param string $name * @return array */ public function getExchangeBindings($name) @@ -82,6 +119,8 @@ public function getConnections() } /** + * Clear Queue + * * @param string $name * @param int $numMessages * @return string @@ -101,7 +140,7 @@ public function clearQueue(string $name, int $numMessages = 50) /** * Delete connection * - * @param $name + * @param string $name * @return string $data */ public function deleteConnection($name) diff --git a/dev/tests/integration/framework/Magento/TestFramework/Lock/Backend/DummyLocker.php b/dev/tests/integration/framework/Magento/TestFramework/Lock/Backend/DummyLocker.php new file mode 100644 index 0000000000000..41125493643e3 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Lock/Backend/DummyLocker.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Lock\Backend; + +use Magento\Framework\Lock\LockManagerInterface; + +/** + * Dummy locker for the integration framework. + */ +class DummyLocker implements LockManagerInterface +{ + /** + * @inheritdoc + */ + public function lock(string $name, int $timeout = -1): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function unlock(string $name): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function isLocked(string $name): bool + { + return false; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php index 14847ae506622..32240e68ae73e 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php @@ -12,6 +12,9 @@ use Magento\Framework\OsInfo; use Magento\TestFramework\Helper\Amqp; +/** + * Publisher Consumer Controller + */ class PublisherConsumerController { /** @@ -49,6 +52,16 @@ class PublisherConsumerController */ private $amqpHelper; + /** + * PublisherConsumerController constructor. + * @param PublisherInterface $publisher + * @param OsInfo $osInfo + * @param Amqp $amqpHelper + * @param string $logFilePath + * @param array $consumers + * @param array $appInitParams + * @param null|int $maxMessages + */ public function __construct( PublisherInterface $publisher, OsInfo $osInfo, @@ -75,27 +88,16 @@ public function __construct( */ public function initialize() { - if ($this->osInfo->isWindows()) { - throw new EnvironmentPreconditionException( - "This test relies on *nix shell and should be skipped in Windows environment." - ); - } + $this->validateEnvironmentPreconditions(); + $connections = $this->amqpHelper->getConnections(); foreach (array_keys($connections) as $connectionName) { $this->amqpHelper->deleteConnection($connectionName); } $this->amqpHelper->clearQueue("async.operations.all"); - foreach ($this->consumers as $consumer) { - foreach ($this->getConsumerProcessIds($consumer) as $consumerProcessId) { - exec("kill {$consumerProcessId}"); - } - } - foreach ($this->consumers as $consumer) { - if (!$this->getConsumerProcessIds($consumer)) { - exec("{$this->getConsumerStartCommand($consumer, true)} > /dev/null &"); - } - sleep(5); - } + + $this->stopConsumers(); + $this->startConsumers(); if (file_exists($this->logFilePath)) { // try to remove before failing the test @@ -108,6 +110,27 @@ public function initialize() } } + /** + * Validate environment preconditions + * + * @throws EnvironmentPreconditionException + * @throws PreconditionFailedException + */ + private function validateEnvironmentPreconditions() + { + if ($this->osInfo->isWindows()) { + throw new EnvironmentPreconditionException( + "This test relies on *nix shell and should be skipped in Windows environment." + ); + } + + if (!$this->amqpHelper->isAvailable()) { + throw new PreconditionFailedException( + 'This test relies on RabbitMQ Management Plugin.' + ); + } + } + /** * Stop Consumers */ @@ -121,6 +144,8 @@ public function stopConsumers() } /** + * Get Consumers ProcessIds + * * @return array */ public function getConsumersProcessIds() @@ -133,6 +158,8 @@ public function getConsumersProcessIds() } /** + * Get Consumer ProcessIds + * * @param string $consumer * @return string[] */ @@ -167,8 +194,10 @@ private function getConsumerStartCommand($consumer, $withEnvVariables = false) } /** + * Wait for asynchronous result + * * @param callable $condition - * @param $params + * @param array $params * @throws PreconditionFailedException */ public function waitForAsynchronousResult(callable $condition, $params) @@ -185,10 +214,27 @@ public function waitForAsynchronousResult(callable $condition, $params) } /** + * Get publisher + * * @return PublisherInterface */ public function getPublisher() { return $this->publisher; } + + /** + * Start consumers + * + * @return void + */ + public function startConsumers(): void + { + foreach ($this->consumers as $consumer) { + if (!$this->getConsumerProcessIds($consumer)) { + exec("{$this->getConsumerStartCommand($consumer, true)} > /dev/null &"); + } + sleep(5); + } + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php b/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php new file mode 100644 index 0000000000000..136b0565a729a --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Session; + +/** + * Class to check if session can be started or not. Dummy for integration tests. + */ +class SessionStartChecker extends \Magento\Framework\Session\SessionStartChecker +{ + /** + * Can session be started or not. + * + * @return bool + */ + public function check() : bool + { + return true; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index b2e0b57bae729..7a387bd41eec2 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -5,8 +5,6 @@ */ namespace Magento\TestFramework\TestCase; -use Magento\Framework\App\Request\Http as HttpRequest; - /** * A parent class for backend controllers - contains directives for admin user creation and authentication. * @@ -122,7 +120,7 @@ public function testAclHasAccess() */ public function testAclNoAccess() { - if ($this->resource === null) { + if ($this->resource === null || $this->uri === null) { $this->markTestIncomplete('Acl test is not complete'); } if ($this->httpMethod) { diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php index 22240ad8e1fe9..00af4419e1142 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Test\Annotation; +use Magento\Framework\Component\ComponentRegistrar; + /** * Test class for \Magento\TestFramework\Annotation\DataFixture. * @@ -178,4 +180,16 @@ public function testRollbackTransactionRevertFixtureFile() ); $this->_object->rollbackTransaction(); } + + /** + * @magentoDataFixture Foo_DataFixtureDummy::Test/Integration/foo.php + * @SuppressWarnings(PHPMD.StaticAccess) + */ + public function testModuleDataFixture() + { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Foo_DataFixtureDummy', __DIR__); + $this->_object->expects($this->once())->method('_applyOneFixture') + ->with(__DIR__ . '/Test/Integration/foo.php'); + $this->_object->startTransaction($this); + } } diff --git a/dev/tests/integration/phpunit.xml.dist b/dev/tests/integration/phpunit.xml.dist index b54a504a1bebc..815abde6ac26b 100644 --- a/dev/tests/integration/phpunit.xml.dist +++ b/dev/tests/integration/phpunit.xml.dist @@ -72,6 +72,8 @@ <!-- Connection parameters for MongoDB library tests --> <!--<const name="MONGODB_CONNECTION_STRING" value="mongodb://localhost:27017"/>--> <!--<const name="MONGODB_DATABASE_NAME" value="magento_integration_tests"/>--> + <!-- Connection parameters for RabbitMQ tests --> + <!--<const name="RABBITMQ_MANAGEMENT_PORT" value="15672"/>--> </php> <!-- Test listeners --> <listeners> diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php index 305c3550269da..c0cc1763b2654 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php @@ -128,7 +128,10 @@ public function sendBulk($products) } $this->clearProducts(); - $result = $this->massSchedule->publishMass('async.V1.products.POST', $products); + $result = $this->massSchedule->publishMass( + 'async.magento.catalog.api.productrepositoryinterface.save.post', + $products + ); //assert bulk accepted with no errors $this->assertFalse($result->isErrors()); @@ -206,7 +209,7 @@ public function testScheduleMassOneEntityFailure($products) $expectedErrorMessage = "Data item corresponding to \"product\" " . "must be specified in the message with topic " . - "\"async.V1.products.POST\"."; + "\"async.magento.catalog.api.productrepositoryinterface.save.post\"."; $this->assertEquals( $expectedErrorMessage, $reasonException->getMessage() diff --git a/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php b/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php index 28d4395e7e413..7ab55dc7fd928 100644 --- a/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php +++ b/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php @@ -23,7 +23,7 @@ public function testUnauthorizedRequest() $data = [ 'x_response_code' => 1, 'x_response_reason_code' => 1, - 'x_invoice_num' => 1, + 'x_invoice_num' => '1', 'x_amount' => 16, 'x_trans_id' => '32iiw5ve', 'x_card_type' => 'American Express', @@ -48,7 +48,7 @@ public function testSuccess() $data = [ 'x_response_code' => 1, 'x_response_reason_code' => 1, - 'x_invoice_num' => 1, + 'x_invoice_num' => '1', 'x_amount' => 16, 'x_trans_id' => '32iiw5ve', 'x_card_type' => 'American Express', diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php new file mode 100644 index 0000000000000..4ef5a4dd14c08 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment\Transaction; +use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; + +$order = include __DIR__ . '/../_files/full_order.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Payment $payment */ +$payment = $order->getPayment(); +$payment->setMethod(Config::METHOD); +$payment->setAuthorizationTransaction(false); +$payment->setParentTransactionId(4321); + + +/** @var OrderRepository $orderRepo */ +$orderRepo = $objectManager->get(OrderRepository::class); +$orderRepo->save($order); + +/** @var TransactionBuilder $transactionBuilder */ +$transactionBuilder = $objectManager->create(TransactionBuilder::class); +$transactionAuthorize = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(1234) + ->build(Transaction::TYPE_AUTH); +$transactionCapture = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(4321) + ->build(Transaction::TYPE_CAPTURE); + +$transactionRepository = $objectManager->create(TransactionRepositoryInterface::class); +$transactionRepository->save($transactionAuthorize); +$transactionRepository->save($transactionCapture); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture_rollback.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture_rollback.php new file mode 100644 index 0000000000000..1a2cb2532fe52 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\ObjectManager; + +$objectManager = ObjectManager::getInstance(); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('increment_id', '100000001') + ->create(); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$items = $orderRepository->getList($searchCriteria) + ->getItems(); + +foreach ($items as $item) { + $orderRepository->delete($item); +} + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php new file mode 100644 index 0000000000000..b1d0521c9c610 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment\Transaction; +use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null) + ->setAddressType('shipping'); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple'); + +require __DIR__ . '/payment.php'; + +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000002') + ->setSubtotal($product->getPrice() * 2) + ->setBaseSubtotal($product->getPrice() * 2) + ->setCustomerEmail('admin@example.com') + ->setCustomerIsGuest(true) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId( + $objectManager->get(StoreManagerInterface::class)->getStore() + ->getId() + ) + ->addItem($orderItem) + ->setPayment($payment); + +$payment->setParentTransactionId(1234); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var TransactionBuilder $transactionBuilder */ +$transactionBuilder = $objectManager->create(TransactionBuilder::class); +$transactionAuthorize = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(1234) + ->build(Transaction::TYPE_AUTH); + +$transactionAuthorize->setAdditionalInformation('real_transaction_id', '1234'); + +$transactionRepository = $objectManager->create(TransactionRepositoryInterface::class); +$transactionRepository->save($transactionAuthorize); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only_rollback.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only_rollback.php new file mode 100644 index 0000000000000..5a65a1fc0d0c7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +require __DIR__ . '/order_captured_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured.php new file mode 100644 index 0000000000000..9bfc863df7de5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment\Transaction; +use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null) + ->setAddressType('shipping'); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple'); + +require __DIR__ . '/payment.php'; + +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000002') + ->setSubtotal($product->getPrice() * 2) + ->setBaseSubtotal($product->getPrice() * 2) + ->setCustomerEmail('admin@example.com') + ->setCustomerIsGuest(true) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId( + $objectManager->get(StoreManagerInterface::class)->getStore() + ->getId() + ) + ->addItem($orderItem) + ->setPayment($payment); + +$payment->setParentTransactionId(4321); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var TransactionBuilder $transactionBuilder */ +$transactionBuilder = $objectManager->create(TransactionBuilder::class); +$transactionAuthorize = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(1234) + ->build(Transaction::TYPE_AUTH); +$transactionCapture = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(4321) + ->build(Transaction::TYPE_CAPTURE); + +$transactionRepository = $objectManager->create(TransactionRepositoryInterface::class); +$transactionRepository->save($transactionAuthorize); +$transactionRepository->save($transactionCapture); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured_rollback.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured_rollback.php new file mode 100644 index 0000000000000..a2da0b639e98d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\ObjectManager; + +$objectManager = ObjectManager::getInstance(); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('increment_id', '100000002') + ->create(); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$items = $orderRepository->getList($searchCriteria) + ->getItems(); + +foreach ($items as $item) { + $orderRepository->delete($item); +} + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/payment.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/payment.php new file mode 100644 index 0000000000000..5b15e356a7d8d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/payment.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Sales\Model\Order\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(Config::METHOD); +$payment->setAuthorizationTransaction(true); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/AbstractTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/AbstractTest.php new file mode 100644 index 0000000000000..f1458a19012f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/AbstractTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Area; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Payment\Gateway\Data\PaymentDataObjectFactory; +use Magento\Quote\Model\Quote\PaymentFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Zend_Http_Response; + +abstract class AbstractTest extends TestCase +{ + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var ZendClient|MockObject|InvocationMocker + */ + protected $clientMock; + + /** + * @var PaymentFactory + */ + protected $paymentFactory; + + /** + * @var Zend_Http_Response + */ + protected $responseMock; + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function setUp() + { + $bootstrap = Bootstrap::getInstance(); + $bootstrap->loadArea(Area::AREA_FRONTEND); + $this->objectManager = Bootstrap::getObjectManager(); + $this->clientMock = $this->createMock(ZendClient::class); + $this->responseMock = $this->createMock(Zend_Http_Response::class); + $this->clientMock->method('request') + ->willReturn($this->responseMock); + $this->clientMock->method('setUri') + ->with('https://apitest.authorize.net/xml/v1/request.api'); + $clientFactoryMock = $this->createMock(ZendClientFactory::class); + $clientFactoryMock->method('create') + ->willReturn($this->clientMock); + /** @var PaymentDataObjectFactory $paymentFactory */ + $this->paymentFactory = $this->objectManager->get(PaymentDataObjectFactory::class); + $this->objectManager->addSharedInstance($clientFactoryMock, ZendClientFactory::class); + } + + protected function tearDown() + { + $this->objectManager->removeSharedInstance(ZendClientFactory::class); + parent::tearDown(); + } + + protected function getOrderWithIncrementId(string $incrementId): Order + { + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $searchCriteria = $this->objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', $incrementId) + ->create(); + /** @var Order $order */ + $order = current( + $orderRepository->getList($searchCriteria) + ->getItems() + ); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptFdsCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptFdsCommandTest.php new file mode 100644 index 0000000000000..394d9de6684c4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptFdsCommandTest.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class AcceptFdsCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testAcceptFdsCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('accept_fds'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/accept_fds.php'; + $response = include __DIR__ . '/../../_files/response/generic_success.php'; + + $this->clientMock->expects($this->once()) + ->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->expects($this->once()) + ->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO + ]); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AuthorizeCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AuthorizeCommandTest.php new file mode 100644 index 0000000000000..9affd80be0600 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AuthorizeCommandTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; + +class AuthorizeCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + */ + public function testAuthorizeCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('authorize'); + + $order = include __DIR__ . '/../../_files/full_order.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/authorize.php'; + $response = include __DIR__ . '/../../_files/response/authorize.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('Y', $payment->getCcAvsStatus()); + $this->assertFalse($payment->getData('is_transaction_closed')); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/CancelCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/CancelCommandTest.php new file mode 100644 index 0000000000000..aa606a50ae67a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/CancelCommandTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class CancelCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + * @dataProvider aliasesProvider + */ + public function testCancelCommand(string $commandName) + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get($commandName); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/void.php'; + $response = include __DIR__ . '/../../_files/response/void.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO + ]); + + /** @var Payment $payment */ + + $this->assertTrue($payment->getIsTransactionClosed()); + $this->assertTrue($payment->getShouldCloseParentTransaction()); + $this->assertArrayNotHasKey('real_transaction_id', $payment->getTransactionAdditionalInfo()); + } + + public function aliasesProvider() + { + return [ + ['cancel'], + ['deny_payment'] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommandTest.php new file mode 100644 index 0000000000000..1651dfc7db3d9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommandTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class FetchTransactionInfoCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionApproved() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_authorized.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertTrue($payment->getIsTransactionApproved()); + $this->assertFalse($payment->getIsTransactionDenied()); + } + + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionVoided() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_voided.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertFalse($payment->getIsTransactionApproved()); + $this->assertTrue($payment->getIsTransactionDenied()); + } + + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionDenied() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_voided.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertFalse($payment->getIsTransactionApproved()); + $this->assertTrue($payment->getIsTransactionDenied()); + } + + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionPending() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_fds_pending.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertNull($payment->getIsTransactionApproved()); + $this->assertNull($payment->getIsTransactionDenied()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php new file mode 100644 index 0000000000000..6e06d749f3906 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class RefundSettledCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php + */ + public function testRefundSettledCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('refund_settled'); + + $order = $this->getOrderWithIncrementId('100000001'); + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/refund.php'; + $response = include __DIR__ . '/../../_files/response/refund.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $this->assertTrue($payment->getIsTransactionClosed()); + $this->assertSame('5678', $payment->getTransactionId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SaleCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SaleCommandTest.php new file mode 100644 index 0000000000000..7ae03d36cb752 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SaleCommandTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; + +class SaleCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + */ + public function testSaleCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('sale'); + + $order = include __DIR__ . '/../../_files/full_order.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/sale.php'; + $response = include __DIR__ . '/../../_files/response/sale.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('Y', $payment->getCcAvsStatus()); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + $this->assertTrue($payment->getShouldCloseParentTransaction()); + $this->assertFalse($payment->getData('is_transaction_closed')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SettleCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SettleCommandTest.php new file mode 100644 index 0000000000000..bb0a259b165bf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SettleCommandTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class SettleCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testRefundSettledCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('settle'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/settle.php'; + $response = include __DIR__ . '/../../_files/response/settle.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $this->assertTrue($payment->getShouldCloseParentTransaction()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/TransactionDetailsCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/TransactionDetailsCommandTest.php new file mode 100644 index 0000000000000..d81cffc413b59 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/TransactionDetailsCommandTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; + +class TransactionDetailsCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_captured.php + */ + public function testTransactionDetails() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('get_transaction_details'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_settled_capture.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $resultData = $result->get(); + + $this->assertEquals($response, $resultData); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/VoidCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/VoidCommandTest.php new file mode 100644 index 0000000000000..f74f8542bfdc3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/VoidCommandTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class VoidCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testVoidCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('void'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/void.php'; + $response = include __DIR__ . '/../../_files/response/void.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO + ]); + + /** @var Payment $payment */ + + $this->assertTrue($payment->getIsTransactionClosed()); + $this->assertTrue($payment->getShouldCloseParentTransaction()); + $this->assertEquals('1234', $payment->getTransactionAdditionalInfo()['real_transaction_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php new file mode 100644 index 0000000000000..a37f927274242 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway; + +use Magento\Framework\Config\Data; +use Magento\Payment\Model\Method\Adapter; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +class ConfigTest extends TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + public function testVerifyConfiguration() + { + /** @var Adapter $paymentAdapter */ + $paymentAdapter = $this->objectManager->get('AuthorizenetAcceptjsFacade'); + + $this->assertEquals('authorizenet_acceptjs', $paymentAdapter->getCode()); + $this->assertTrue($paymentAdapter->canAuthorize()); + $this->assertTrue($paymentAdapter->canCapture()); + $this->assertFalse($paymentAdapter->canCapturePartial()); + $this->assertTrue($paymentAdapter->canRefund()); + $this->assertTrue($paymentAdapter->canUseCheckout()); + $this->assertTrue($paymentAdapter->canVoid()); + $this->assertTrue($paymentAdapter->canUseInternal()); + $this->assertTrue($paymentAdapter->canEdit()); + $this->assertTrue($paymentAdapter->canFetchTransactionInfo()); + + /** @var Data $configReader */ + $configReader = $this->objectManager->get('Magento\Payment\Model\Config\Data'); + $value = $configReader->get('methods/authorizenet_acceptjs/allow_multiple_address'); + + $this->assertSame('0', $value); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/accept_fds.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/accept_fds.php new file mode 100644 index 0000000000000..d843de1c2cac0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/accept_fds.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'updateHeldTransactionRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword' + ], + 'heldTransactionRequest' => [ + 'action' => 'approve', + 'refTransId' => '1234', + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/authorize.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/authorize.php new file mode 100644 index 0000000000000..16debdb2ef820 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/authorize.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'authOnlyTransaction', + 'amount' => '100.00', + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'mydescriptor', + 'dataValue' => 'myvalue', + ], + ], + 'solution' => [ + 'id' => 'AAA102993', + ], + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => 1, + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1', + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'authOnlyTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/refund.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/refund.php new file mode 100644 index 0000000000000..5ed331d076f66 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/refund.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'refundTransaction', + 'amount' => '100.00', + 'payment' => [ + 'creditCard' => [ + 'cardNumber' => '1111', + 'expirationDate' => 'XXXX' + ] + ], + 'refTransId' => '4321', + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => '1', + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1' + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/sale.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/sale.php new file mode 100644 index 0000000000000..4514acbcb6646 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/sale.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'authCaptureTransaction', + 'amount' => '100.00', + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'mydescriptor', + 'dataValue' => 'myvalue', + ], + ], + 'solution' => [ + 'id' => 'AAA102993', + ], + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => 1, + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1', + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'authCaptureTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/settle.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/settle.php new file mode 100644 index 0000000000000..b4fa88cc1e5a9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/settle.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'priorAuthCaptureTransaction', + 'refTransId' => '1234', + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'priorAuthCaptureTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details.php new file mode 100644 index 0000000000000..110333866766e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'getTransactionDetailsRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword' + ], + 'transId' => '4321' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details_authorized.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details_authorized.php new file mode 100644 index 0000000000000..c3ffdedba6851 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details_authorized.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'getTransactionDetailsRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword' + ], + 'transId' => '1234' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/void.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/void.php new file mode 100644 index 0000000000000..a1d3dade74ff1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/void.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' =>[ + 'transactionType' => 'voidTransaction', + 'refTransId' => '1234', + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php new file mode 100644 index 0000000000000..cac7c38971ae5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item; +use Magento\TestFramework\Helper\Bootstrap; + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; +require __DIR__ . '/../../../Magento/Customer/_files/customer.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription('Short description') + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ])->setCanSaveCustomOptions(true) + ->setHasOptions(false); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + + +$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null) + ->setAddressType('shipping') + ->setStreet(['6161 West Centinela Avenue']) + ->setFirstname('John') + ->setLastname('Doe') + ->setShippingMethod('flatrate_flatrate'); + +$payment = $objectManager->create(Payment::class); +$payment->setAdditionalInformation('ccLast4', '1111'); +$payment->setAdditionalInformation('opaqueDataDescriptor', 'mydescriptor'); +$payment->setAdditionalInformation('opaqueDataValue', 'myvalue'); + +/** @var Item $orderItem */ +$orderItem1 = $objectManager->create(Item::class); +$orderItem1->setProductId($product->getId()) + ->setSku($product->getSku()) + ->setName($product->getName()) + ->setQtyOrdered(1) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType($product->getTypeId()); + +/** @var Item $orderItem */ +$orderItem2 = $objectManager->create(Item::class); +$orderItem2->setProductId($product->getId()) + ->setSku('simple2') + ->setName('Simple product') + ->setPrice(100) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType($product->getTypeId()); + +$orderAmount = 100; +$customerEmail = $billingAddress->getEmail(); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus(Order::STATE_PROCESSING) + ->setCustomerId($customer->getId()) + ->setCustomerIsGuest(false) + ->setRemoteIp('127.0.0.1') + ->setCreatedAt(date('Y-m-d 00:00:55')) + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setSubtotal($orderAmount) + ->setGrandTotal($orderAmount) + ->setBaseSubtotal($orderAmount) + ->setBaseGrandTotal($orderAmount) + ->setCustomerEmail($customerEmail) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setShippingDescription('Flat Rate - Fixed') + ->setShippingAmount(10) + ->setStoreId(1) + ->addItem($orderItem1) + ->addItem($orderItem2) + ->setQuoteId(1) + ->setPayment($payment); + +return $order; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/authorize.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/authorize.php new file mode 100644 index 0000000000000..f80495137ca29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/authorize.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'transId' => '123456', + 'refTransID' => '', + 'transHash' => 'foobar', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'userFields' => [ + [ + 'name' => 'transactionType', + 'value' => 'authOnlyTransaction' + ] + ], + 'transHashSha2' => 'CD1E57FB1B5C876FDBD536CB16F8BBBA687580EDD78DD881C7F14DC4467C32BF6C' + . '808620FBD59E5977DF19460B98CCFC0DA0D90755992C0D611CABB8E2BA52B0', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/generic_success.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/generic_success.php new file mode 100644 index 0000000000000..ea7662e319376 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/generic_success.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/refund.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/refund.php new file mode 100644 index 0000000000000..536f51d659ad8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/refund.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => '', + 'avsResultCode' => 'P', + 'cvvResultCode' => '', + 'cavvResultCode' => '', + 'transId' => '5678', + 'refTransID' => '4321', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'transHashSha2' => '78BD31BA5BCDF3C3FA3C8373D8DF80EF07FC7E02C3545FCF18A408E2F76ED4F20D' + . 'FF007221374B576FDD1BFD953B3E5CF37249CEC4C135EEF975F7B478D8452C', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/sale.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/sale.php new file mode 100644 index 0000000000000..74a80110adece --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/sale.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'transId' => '123456', + 'refTransID' => '', + 'transHash' => 'foobar', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'userFields' => [ + [ + 'name' => 'transactionType', + 'value' => 'authCaptureTransaction' + ] + ], + 'transHashSha2' => 'CD1E57FB1B5C876FDBD536CB16F8BBBA687580EDD78DD881C7F14DC4467C32BF6C' + . '808620FBD59E5977DF19460B98CCFC0DA0D90755992C0D611CABB8E2BA52B0', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/settle.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/settle.php new file mode 100644 index 0000000000000..5e54c30198741 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/settle.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => '', + 'avsResultCode' => 'P', + 'cvvResultCode' => '', + 'cavvResultCode' => '', + 'transId' => '1234', + 'refTransID' => '1234', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'transHashSha2' => '1B22AB4E4DF750CF2E0D1944BB6903537C145545C7313C87B6FD4A6384' + . '709EA2609CE9A9788C128F2F2EAEEE474F6010418904648C6D000BE3AF7BCD98A5AD8F', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_authorized.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_authorized.php new file mode 100644 index 0000000000000..80fd24a5c601a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_authorized.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'authorizedPendingCapture' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_declined.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_declined.php new file mode 100644 index 0000000000000..24c9353e4088a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_declined.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'declined' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_fds_pending.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_fds_pending.php new file mode 100644 index 0000000000000..de045f30ab22e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_fds_pending.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'FDSPendingReview' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_settled_capture.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_settled_capture.php new file mode 100644 index 0000000000000..5df2f03a943a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_settled_capture.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '4321', + 'transactionType' => 'captureOnlyTransaction', + 'transactionStatus' => 'settledSuccessfully' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_voided.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_voided.php new file mode 100644 index 0000000000000..7ee735cd8cf36 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_voided.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'void' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/void.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/void.php new file mode 100644 index 0000000000000..eb71de4dd9667 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/void.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transactionResponse' => [ + 'responseCode' => '1', + 'messages' => [ + 'message' => [ + [ + 'code' => 1 + ] + ] + ], + 'transHashSha2' => '1B22AB4E4DF750CF2E0D1944BB6903537C145545C7313C87B6FD4A6384709E' + . 'A2609CE9A9788C128F2F2EAEEE474F6010418904648C6D000BE3AF7BCD98A5AD8F', + 'transId' => '1234' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php index 17863cd709580..497deb2c99110 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Block\Dashboard; /** @@ -27,6 +29,6 @@ protected function setUp() public function testGetChartUrl() { - $this->assertStringStartsWith('http://chart.apis.google.com/chart', $this->_block->getChartUrl()); + $this->assertStringStartsWith('https://image-charts.com/chart', $this->_block->getChartUrl()); } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php index 8aeee9cf12494..e11c5ce5d9cf3 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php @@ -87,41 +87,6 @@ public function testMassactionDefaultValues() $this->assertFalse($blockEmpty->isAvailable()); } - public function testGetJavaScript() - { - $this->loadLayout(); - - $javascript = $this->_block->getJavaScript(); - - $expectedItemFirst = '#"option_id1":{"label":"Option One",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"complete":"Test","id":"option_id1"}#'; - $this->assertRegExp($expectedItemFirst, $javascript); - - $expectedItemSecond = '#"option_id2":{"label":"Option Two",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"confirm":"Are you sure\?","id":"option_id2"}#'; - $this->assertRegExp($expectedItemSecond, $javascript); - } - - public function testGetJavaScriptWithAddedItem() - { - $this->loadLayout(); - - $input = [ - 'id' => 'option_id3', - 'label' => 'Option Three', - 'url' => '*/*/option3', - 'block_name' => 'admin.test.grid.massaction.option3', - ]; - $expected = '#"option_id3":{"id":"option_id3","label":"Option Three",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"block_name":"admin.test.grid.massaction.option3"}#'; - - $this->_block->addItem($input['id'], $input); - $this->assertRegExp($expected, $this->_block->getJavaScript()); - } - /** * @param string $mageMode * @param int $expectedCount @@ -213,21 +178,4 @@ public function getItemsDataProvider() ] ]; } - - public function testGridContainsMassactionColumn() - { - $this->loadLayout(); - $this->_layout->getBlock('admin.test.grid')->toHtml(); - - $gridMassactionColumn = $this->_layout->getBlock('admin.test.grid') - ->getColumnSet() - ->getChildBlock('massaction'); - - $this->assertNotNull($gridMassactionColumn, 'Massaction column does not exist in the grid column set'); - $this->assertInstanceOf( - \Magento\Backend\Block\Widget\Grid\Column::class, - $gridMassactionColumn, - 'Massaction column is not an instance of \Magento\Backend\Block\Widget\Column' - ); - } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php index 595a33344c7e8..bd4dd0c8daf0c 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php @@ -4,8 +4,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Controller\Adminhtml\Dashboard; +/** + * Test product viewed backend controller. + */ class ProductsViewedTest extends \Magento\TestFramework\TestCase\AbstractBackendController { /** @@ -14,6 +18,7 @@ class ProductsViewedTest extends \Magento\TestFramework\TestCase\AbstractBackend */ public function testExecute() { + $this->getRequest()->setMethod("POST"); $this->dispatch('backend/admin/dashboard/productsViewed/'); $this->assertEquals(200, $this->getResponse()->getHttpResponseCode()); diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php index 07af21505f180..0eb98379b4571 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Controller\Adminhtml; /** @@ -19,8 +21,15 @@ public function testAjaxBlockAction() $this->assertContains('dashboard-diagram', $actual); } + /** + * Tests tunnelAction + * + * @throws \Exception + * @return void + */ public function testTunnelAction() { + // phpcs:disable Magento2.Functions.DiscouragedFunction $testUrl = \Magento\Backend\Block\Dashboard\Graph::API_URL . '?cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'; $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $testUrl); @@ -34,6 +43,7 @@ public function testTunnelAction() curl_close($handle); throw $e; } + // phpcs:enable $gaData = [ 'cht' => 'lc', diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/ReviewTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/ReviewTest.php new file mode 100644 index 0000000000000..fc79048f15f45 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/ReviewTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Controller\Paypal; + +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * ReviewTest + */ +class ReviewTest extends AbstractController +{ + /** + * @var Review + */ + private $controller; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->controller = $this->_objectManager->create(Review::class); + } + + /** + * Test controller implements correct interfaces + * + */ + public function testInterfaceImplementation() + { + $this->assertInstanceOf(HttpGetActionInterface::class, $this->controller); + $this->assertInstanceOf(HttpPostActionInterface::class, $this->controller); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php new file mode 100644 index 0000000000000..91cea7dc96602 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; + +$store = $storeManager->getStore(); +$quote->setReservedOrderId('multishipping_quote_id_braintree') + ->setStoreId($store->getId()) + ->setCustomerEmail('customer001@test.com'); + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +$quote->collectTotals(); +$quoteRepository->save($quote); + +$items = $quote->getAllItems(); +$addressList = $quote->getAllShippingAddresses(); + +foreach ($addressList as $key => $address) { + $item = $items[$key]; + // set correct quantity per shipping address + $item->setQty(1); + $address->setTotalQty(1); + $address->addItem($item); +} + +// assign virtual product to the billing address +$billingAddress = $quote->getBillingAddress(); +$virtualItem = $items[sizeof($items) - 1]; +$billingAddress->setTotalQty(1); +$billingAddress->addItem($virtualItem); + +// need to recollect totals +$quote->setTotalsCollectedFlag(false); +$quote->collectTotals(); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php new file mode 100644 index 0000000000000..3e1db90f1f2c8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Braintree\Model\Ui\ConfigProvider; + +/** + * @var Magento\Quote\Model\Quote $quote + */ + +if (empty($quote)) { + throw new \Exception('$quote should be defined in the parent fixture'); +} + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(ConfigProvider::CODE); +$quote->setPayment($payment); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php new file mode 100644 index 0000000000000..e4bba222078b0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; + +/** + * @var Magento\Quote\Model\Quote $quote + */ + +if (empty($quote)) { + throw new \Exception('$quote should be defined in the parent fixture'); +} + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(ConfigProvider::PAYPAL_CODE); +$quote->setPayment($payment); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php new file mode 100644 index 0000000000000..1c56e611dd6db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); + +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/shipping_address_list.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/billing_address.php'; +require __DIR__ . '/payment_braintree.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/items.php'; +require __DIR__ . '/assign_items_per_address.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php new file mode 100644 index 0000000000000..4bd8e926abb76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); + +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/shipping_address_list.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/billing_address.php'; +require __DIR__ . '/payment_braintree_paypal.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/items.php'; +require __DIR__ . '/assign_items_per_address.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php new file mode 100644 index 0000000000000..91bc0388d8551 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php @@ -0,0 +1,254 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Model; + +use Braintree\Result\Successful; +use Braintree\Transaction; +use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Multishipping\Model\Checkout\Type\Multishipping; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use \PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Payment\Gateway\Command\ResultInterface as CommandResultInterface; + +/** + * Tests Magento\Multishipping\Model\Checkout\Type\Multishipping with Braintree and BraintreePayPal payments. + * + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MultishippingTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapter; + + /** + * @var Multishipping + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + + $orderSender = $this->getMockBuilder(OrderSender::class) + ->disableOriginalConstructor() + ->getMock(); + + $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactory->method('create') + ->willReturn($this->adapter); + + $this->objectManager->addSharedInstance($adapterFactory, BraintreeAdapterFactory::class); + $this->objectManager->addSharedInstance($this->getPaymentNonceMock(), GetPaymentNonceCommand::class); + + $this->model = $this->objectManager->create( + Multishipping::class, + ['orderSender' => $orderSender] + ); + } + + /** + * Checks a case when multiple orders are created successfully using Braintree payment method. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/quote_with_split_items_braintree.php + * @magentoConfigFixture current_store payment/braintree/active 1 + * @return void + */ + public function testCreateOrdersWithBraintree() + { + $this->adapter->method('sale') + ->willReturn( + $this->getTransactionStub() + ); + $this->createOrders(); + } + + /** + * Checks a case when multiple orders are created successfully using Braintree PayPal payment method. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php + * @magentoConfigFixture current_store payment/braintree_paypal/active 1 + * @return void + */ + public function testCreateOrdersWithBraintreePaypal() + { + $this->adapter->method('sale') + ->willReturn( + $this->getTransactionPaypalStub() + ); + $this->createOrders(); + } + + /** + * Creates orders for multishipping checkout flow. + * + * @return void + */ + private function createOrders() + { + $expectedPlacedOrdersNumber = 3; + $quote = $this->getQuote('multishipping_quote_id_braintree'); + + /** @var CheckoutSession $session */ + $session = $this->objectManager->get(CheckoutSession::class); + $session->replaceQuote($quote); + + $this->model->createOrders(); + + $orderList = $this->getOrderList((int)$quote->getId()); + self::assertCount( + $expectedPlacedOrdersNumber, + $orderList, + 'Total successfully placed orders number mismatch' + ); + } + + /** + * Creates stub for Braintree capture Transaction. + * + * @return Successful + */ + private function getTransactionStub(): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = 'submitted_for_settlement'; + $transaction->creditCard = [ + 'last4' => '1111', + 'cardType' => 'Visa', + 'expirationMonth' => '12', + 'expirationYear' => '2021' + ]; + + $creditCardDetails = new \stdClass(); + $creditCardDetails->token = '4fdg'; + $creditCardDetails->expirationMonth = '12'; + $creditCardDetails->expirationYear = '2021'; + $creditCardDetails->cardType = 'Visa'; + $creditCardDetails->last4 = '1111'; + $creditCardDetails->expirationDate = '12/2021'; + $transaction->creditCardDetails = $creditCardDetails; + + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } + + /** + * Creates stub for BraintreePaypal capture Transaction. + * + * @return Successful + */ + private function getTransactionPaypalStub(): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = 'submitted_for_settlement'; + $transaction->paypal = [ + 'token' => 'fchxqx', + 'payerEmail' => 'payer@example.com', + 'paymentId' => 'PAY-33ac47a28e7f54791f6cda45', + ]; + $paypalDetails = new \stdClass(); + $paypalDetails->token = 'fchxqx'; + $paypalDetails->payerEmail = 'payer@example.com'; + $paypalDetails->paymentId = '33ac47a28e7f54791f6cda45'; + $transaction->paypalDetails = $paypalDetails; + + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } + + /** + * Retrieves quote by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } + + /** + * Get list of orders by quote id. + * + * @param int $quoteId + * @return array + */ + private function getOrderList(int $quoteId): array + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('quote_id', $quoteId) + ->create(); + + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + return $orderRepository->getList($searchCriteria)->getItems(); + } + + /** + * Returns GetPaymentNonceCommand command mock. + * + * @return MockObject + */ + private function getPaymentNonceMock(): MockObject + { + $commandResult = $this->createMock(CommandResultInterface::class); + $commandResult->method('get') + ->willReturn(['paymentMethodNonce' => 'testNonce']); + $paymentNonce = $this->createMock(GetPaymentNonceCommand::class); + $paymentNonce->method('execute') + ->willReturn($commandResult); + + return $paymentNonce; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/Frontend/ProductTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/Frontend/ProductTest.php new file mode 100644 index 0000000000000..91dcd5f3e8d5b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/Frontend/ProductTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Bundle\Model\Plugin\Frontend; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use PHPUnit\Framework\TestCase; + +/** + * Test bundle fronted product plugin adds children products ids to bundle product identities. + */ +class ProductTest extends TestCase +{ + /** + * Check, product plugin is registered for storefront. + * + * @magentoAppArea frontend + * @return void + */ + public function testProductIsRegistered(): void + { + $pluginInfo = Bootstrap::getObjectManager()->get(PluginList::class) + ->get(\Magento\Catalog\Model\Product::class, []); + $this->assertSame(Product::class, $pluginInfo['bundle']['instance']); + } + + /** + * Check plugin will add children ids to bundle product identities on storefront. + * + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoAppArea frontend + * @return void + */ + public function testGetIdentitiesForBundleProductOnStorefront(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $simpleProduct = $productRepository->get('simple'); + $expectedIdentities = [ + 'cat_p_' . $bundleProduct->getId(), + 'cat_p', + 'cat_p_' . $simpleProduct->getId(), + + ]; + $this->assertEquals($expectedIdentities, $bundleProduct->getIdentities()); + } + + /** + * Check plugin won't add children ids to bundle product identities in admin area. + * + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoAppArea adminhtml + * @return void + */ + public function testGetIdentitiesForBundleProductInAdminArea(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $expectedIdentities = [ + 'cat_p_' . $bundleProduct->getId(), + ]; + $this->assertEquals($expectedIdentities, $bundleProduct->getIdentities()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php index b97bd9f822666..e9cb2f2d6c9d4 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php @@ -13,7 +13,6 @@ * @magentoDbIsolation disabled * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension - * @magentoAppArea frontend */ class FixedBundlePriceCalculatorWithDimensionTest extends BundlePriceAbstract { diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php index 2a68ff48e5f9a..4a5757aae3134 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php @@ -6,7 +6,7 @@ namespace Magento\Bundle\Model\Product; /** - * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php + * Class to test bundle prices */ class PriceTest extends \PHPUnit\Framework\TestCase { @@ -22,6 +22,9 @@ protected function setUp() ); } + /** + * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php + */ public function testGetTierPrice() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ @@ -37,4 +40,50 @@ public function testGetTierPrice() $this->assertEquals(20.0, $this->_model->getTierPrice(4, $product)); $this->assertEquals(30.0, $this->_model->getTierPrice(5, $product)); } + + /** + * Test calculation final price for bundle product with tire price in simple product + * + * @param float $bundleQty + * @param float $selectionQty + * @param float $finalPrice + * @magentoDataFixture Magento/Bundle/_files/product_with_simple_tier_pricing.php + * @dataProvider getSelectionFinalTotalPriceWithSimpleTierPriceDataProvider + */ + public function testGetSelectionFinalTotalPriceWithSimpleTierPrice( + float $bundleQty, + float $selectionQty, + float $finalPrice + ) { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $simpleProduct = $productRepository->get('simple'); + $simpleProduct->setCustomerGroupId(\Magento\Customer\Model\Group::CUST_GROUP_ALL); + + $this->assertEquals( + $finalPrice, + $this->_model->getSelectionFinalTotalPrice( + $bundleProduct, + $simpleProduct, + $bundleQty, + $selectionQty, + false + ), + 'Tier price calculation for Simple product is wrong' + ); + } + + /** + * @return array + */ + public function getSelectionFinalTotalPriceWithSimpleTierPriceDataProvider(): array + { + return [ + [1, 1, 10], + [2, 1, 8], + [5, 1, 5], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php new file mode 100644 index 0000000000000..30f0978480701 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +/** @var $productFactory Magento\Catalog\Model\ProductFactory */ +$productFactory = $objectManager->create(\Magento\Catalog\Model\ProductFactory::class); +/** @var $bundleProduct \Magento\Catalog\Model\Product */ +$bundleProduct = $productFactory->create(); +$bundleProduct->setTypeId('bundle') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setPriceType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC) + ->setPriceView(1) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ]) + ->setBundleOptionsData( + [ + [ + 'title' => 'Bundle Product Items', + 'default_title' => 'Bundle Product Items', + 'type' => 'checkbox', + 'required' => 1, + 'delete' => '', + ], + ] + ) + ->setBundleSelectionsData( + [[['product_id' => $product->getId(), 'selection_qty' => 1, 'delete' => '']]] + ); +$productRepository->save($bundleProduct); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php new file mode 100644 index 0000000000000..aa661c7412d42 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php index e15f8d47a7bfc..864bdaa2a1331 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php @@ -9,7 +9,10 @@ class BundleTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ // @todo uncomment after MAGETWO-49677 resolved @@ -45,17 +48,13 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { $expectedBundleProductOptions = $expectedProduct->getExtensionAttributes()->getBundleProductOptions(); $actualBundleProductOptions = $actualProduct->getExtensionAttributes()->getBundleProductOptions(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index a2967878402d0..3ec8c806dcbb1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -5,13 +5,49 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\PublisherConsumerController; /** * @magentoAppArea adminhtml */ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** @var PublisherConsumerController */ + private $publisherConsumerController; + private $consumers = ['product_action_attribute.update']; + + protected function setUp() + { + $this->publisherConsumerController = Bootstrap::getObjectManager()->create(PublisherConsumerController::class, [ + 'consumers' => $this->consumers, + 'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt", + 'maxMessages' => null, + 'appInitParams' => Bootstrap::getInstance()->getAppInitParams() + ]); + + try { + $this->publisherConsumerController->startConsumers(); + } catch (\Magento\TestFramework\MessageQueue\EnvironmentPreconditionException $e) { + $this->markTestSkipped($e->getMessage()); + } catch (\Magento\TestFramework\MessageQueue\PreconditionFailedException $e) { + $this->fail( + $e->getMessage() + ); + } + + parent::setUp(); + } + + protected function tearDown() + { + $this->publisherConsumerController->stopConsumers(); + parent::tearDown(); + } + /** * @covers \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::execute * @@ -20,7 +56,7 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr */ public function testSaveActionRedirectsSuccessfully() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); @@ -59,13 +95,14 @@ public function testSaveActionRedirectsSuccessfully() */ public function testSaveActionChangeVisibility($attributes) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepository $repository */ + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); $product->setOrigData(); - $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); $product->save(); /** @var $session \Magento\Backend\Model\Session */ @@ -75,15 +112,29 @@ public function testSaveActionChangeVisibility($attributes) $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); + /** @var \Magento\Catalog\Model\Category $category */ - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = Bootstrap::getObjectManager()->get( \Magento\Catalog\Block\Product\ListProduct::class ); + $this->publisherConsumerController->waitForAsynchronousResult( + function () use ($repository) { + sleep(3); + return $repository->get( + 'simple', + false, + null, + true + )->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE; + }, + [] + ); + $category = $categoryFactory->create()->load(2); $layer = $listProduct->getLayer(); $layer->setCurrentCategory($category); @@ -105,7 +156,7 @@ public function testSaveActionChangeVisibility($attributes) */ public function testValidateActionWithMassUpdate($attributes) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); @@ -156,8 +207,8 @@ public function validateActionDataProvider() public function saveActionVisibilityAttrDataProvider() { return [ - ['arguments' => ['visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH]], - ['arguments' => ['visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_CATALOG]] + ['arguments' => ['visibility' => Visibility::VISIBILITY_BOTH]], + ['arguments' => ['visibility' => Visibility::VISIBILITY_IN_CATALOG]] ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index fe08ec01a9715..e1d3e960593a9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -157,7 +157,7 @@ public function testWrongAttributeCode() $message = $messages->getItemsByType('error')[0]; $this->assertEquals( 'Attribute code "_()&&&?" is invalid. Please use only letters (a-z or A-Z),' - . ' numbers (0-9) or underscore(_) in this field, first character should be a letter.', + . ' numbers (0-9) or underscore (_) in this field, and the first character should be a letter.', $message->getText() ); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php index 5721982ab8dc3..d4926e78040d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/CategoryTest.php @@ -38,8 +38,7 @@ public function testGetProductCollection() /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $collection = $this->_model->getProductCollection(); $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Product\Collection::class, $collection); - $ids = $collection->getAllIds(); - $this->assertEquals(2, count($ids)); + $this->assertEquals(2, $collection->count()); $this->assertSame($collection, $this->_model->getProductCollection()); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php index 280e83a863325..fe39de2729eac 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; @@ -108,7 +109,7 @@ public function testGetFinalPrice() } /** - * Get formated price + * Get formatted price */ public function testGetFormatedPrice() { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index 7954e2c36227f..476f01eb277df 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -12,6 +12,11 @@ class ProductTest extends TestCase { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @var Product */ @@ -29,7 +34,8 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->get(Product::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->model = $this->objectManager->create(Product::class); } /** @@ -42,11 +48,29 @@ public function testGetAttributeRawValue() $sku = 'simple'; $attribute = 'name'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); - + $product = $this->productRepository->get($sku); $actual = $this->model->getAttributeRawValue($product->getId(), $attribute, null); self::assertEquals($product->getName(), $actual); } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store catalog/price/scope 1 + */ + public function testUpdateStoreSpecificSpecialPrice() + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple', true, 1); + $this->assertEquals(5.99, $product->getSpecialPrice()); + + $product->setSpecialPrice(''); + $this->model->save($product); + $product = $this->productRepository->get('simple', false, 1, true); + $this->assertEmpty($product->getSpecialPrice()); + + $product = $this->productRepository->get('simple', false, 0, true); + $this->assertEquals(5.99, $product->getSpecialPrice()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/QuantityAndStockStatusTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/QuantityAndStockStatusTest.php new file mode 100644 index 0000000000000..c09d68a66ee8e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/QuantityAndStockStatusTest.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogInventory\Model\Stock\StockItemRepository; +use Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\CatalogInventory\Api\StockItemCriteriaInterface; +use Magento\CatalogInventory\Api\StockRegistryInterface; + +/** + * Quantity and stock status test + */ +class QuantityAndStockStatusTest extends TestCase +{ + /** + * @var string + */ + private static $quantityAndStockStatus = 'quantity_and_stock_status'; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Test product stock status in the products grid column + * + * @magentoDataFixture Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testProductStockStatus() + { + /** @var StockItemRepository $stockItemRepository */ + $stockItemRepository = $this->objectManager->create(StockItemRepository::class); + + /** @var StockRegistryInterface $stockRegistry */ + $stockRegistry = $this->objectManager->create(StockRegistryInterface::class); + + $stockItem = $stockRegistry->getStockItemBySku('simple'); + $stockItem->setIsInStock(false); + $stockItemRepository->save($stockItem); + $savedStockStatus = (int)$stockItem->getIsInStock(); + + $dataProvider = $this->objectManager->create( + ProductDataProvider::class, + [ + 'name' => 'product_listing_data_source', + 'primaryFieldName' => 'entity_id', + 'requestFieldName' => 'id', + 'addFieldStrategies' => [ + 'quantity_and_stock_status' => + $this->objectManager->get(AddQuantityAndStockStatusFieldToCollection::class) + ] + ] + ); + + $dataProvider->addField(self::$quantityAndStockStatus); + $data = $dataProvider->getData(); + $dataProviderStockStatus = $data['items'][0][self::$quantityAndStockStatus]; + + $this->assertEquals($dataProviderStockStatus, $savedStockStatus); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index a903274793c34..a5ab961932461 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -178,7 +178,7 @@ ->setParentId(3) ->setPath('1/2/3/13') ->setLevel(3) - ->setDescription('Ololo') + ->setDescription('Its a description of Test Category 1.2') ->setAvailableSortBy('name') ->setDefaultSortBy('name') ->setIsActive(true) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php index bd5dc541a15a5..5facba07d58f1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php @@ -187,6 +187,50 @@ 'price_type' => 'percent', 'sku' => 'sku2', 'max_characters' => 20, + ], + [ + 'title' => 'multiple option', + 'type' => 'multiple', + 'is_require' => true, + 'sort_order' => 7, + 'values' => [ + [ + 'title' => 'multiple option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'multiple option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'multiple option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'multiple option 2 sku', + 'sort_order' => 2, + ], + ], + ], + [ + 'title' => 'checkbox option', + 'type' => 'checkbox', + 'is_require' => true, + 'sort_order' => 6, + 'values' => [ + [ + 'title' => 'checkbox option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'checkbox option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'checkbox option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'checkbox option 2 sku', + 'sort_order' => 2, + ], + ], ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php new file mode 100644 index 0000000000000..a401db8eb2bf7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); + +$product->setTypeId( + 'simple' +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [1] +)->setName( + 'Virtual Product With Custom Options' +)->setSku( + 'simple' +)->setPrice( + 10 +)->setMetaTitle( + 'meta title' +)->setMetaKeyword( + 'meta keyword' +)->setMetaDescription( + 'meta description' +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +)->setCanSaveCustomOptions( + true +)->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] +)->setHasOptions(true); + +$options = [ + [ + 'title' => 'test_option_code_1', + 'type' => 'field', + 'is_require' => true, + 'sort_order' => 1, + 'price' => -10.0, + 'price_type' => 'fixed', + 'sku' => 'sku1', + 'max_characters' => 10, + ], + [ + 'title' => 'area option', + 'type' => 'area', + 'is_require' => true, + 'sort_order' => 2, + 'price' => 20.0, + 'price_type' => 'percent', + 'sku' => 'sku2', + 'max_characters' => 20 + ], + [ + 'title' => 'drop_down option', + 'type' => 'drop_down', + 'is_require' => false, + 'sort_order' => 4, + 'values' => [ + [ + 'title' => 'drop_down option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'drop_down option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 2 sku', + 'sort_order' => 2, + ], + ], + ] +]; + +$customOptions = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($options as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $customOption */ + $customOption = $customOptionFactory->create(['data' => $option]); + $customOption->setProductSku($product->getSku()); + + $customOptions[] = $customOption; +} + +$product->setOptions($customOptions); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options_rollback.php new file mode 100644 index 0000000000000..8863da1cd2782 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class +); +try { + $product = $repository->get('simple', false, null, true); + $repository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Entity already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php new file mode 100644 index 0000000000000..a9ab0e11312b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* Delete attribute with text_attribute code */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class +); +$attribute->load('text_attribute', 'attribute_code'); +$attribute->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php index 38e8c404dc002..838ae2b9a2aa6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php @@ -3,9 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; + +$productFactory = Bootstrap::getObjectManager()->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL) ->setId(21) ->setAttributeSetId(4) @@ -22,4 +27,7 @@ 'is_in_stock' => 1, 'manage_stock' => 1, ] - )->save(); + ); +/** @var ProductResource $productResource */ +$productResource = Bootstrap::getObjectManager()->get(ProductResource::class); +$productResource->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php index 7fdeca846885a..f5568ced2c96a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php @@ -3,23 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use Magento\TestFramework\Helper\Bootstrap; + +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); try { $product = $productRepository->get('virtual-product', false, null, true); $productRepository->delete($product); -} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { +} catch (NoSuchEntityException $exception) { //Product already removed -} catch (\Magento\Framework\Exception\StateException $exception) { +} catch (StateException $exception) { } $registry->unregister('isSecureArea'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options.php new file mode 100644 index 0000000000000..c1f981cefa646 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); + +$product->setTypeId( + 'virtual' +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [1] +)->setName( + 'Virtual Product With Custom Options' +)->setSku( + 'virtual' +)->setPrice( + 10 +)->setMetaTitle( + 'meta title' +)->setMetaKeyword( + 'meta keyword' +)->setMetaDescription( + 'meta description' +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +)->setCanSaveCustomOptions( + true +)->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] +)->setHasOptions(true); + +$options = [ + [ + 'title' => 'test_option_code_1', + 'type' => 'field', + 'is_require' => true, + 'sort_order' => 1, + 'price' => -10.0, + 'price_type' => 'fixed', + 'sku' => 'sku1', + 'max_characters' => 10, + ], + [ + 'title' => 'area option', + 'type' => 'area', + 'is_require' => true, + 'sort_order' => 2, + 'price' => 20.0, + 'price_type' => 'percent', + 'sku' => 'sku2', + 'max_characters' => 20 + ], + [ + 'title' => 'drop_down option', + 'type' => 'drop_down', + 'is_require' => false, + 'sort_order' => 4, + 'values' => [ + [ + 'title' => 'drop_down option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'drop_down option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 2 sku', + 'sort_order' => 2, + ], + ], + ] +]; + +$customOptions = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($options as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $customOption */ + $customOption = $customOptionFactory->create(['data' => $option]); + $customOption->setProductSku($product->getSku()); + + $customOptions[] = $customOption; +} + +$product->setOptions($customOptions); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options_rollback.php new file mode 100644 index 0000000000000..f46cdc13d3263 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class +); +try { + $product = $repository->get('virtual', false, null, true); + $repository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Entity already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php index 7d6e2e6f97800..2cd0dd2c77560 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php @@ -15,8 +15,8 @@ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setWebsiteIds([1]) ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) - ->setNewsFromDate(date('Y-m-d', strtotime('-2 day'))) - ->setNewsToDate(date('Y-m-d', strtotime('+2 day'))) + ->setNewsFromDate(date('Y-m-d H:i:s', strtotime('-2 day'))) + ->setNewsToDate(date('Y-m-d H:i:s', strtotime('+2 day'))) ->setDescription('description') ->setShortDescription('short desc') ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php new file mode 100644 index 0000000000000..1870eaba566d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$eavSetupFactory = $objectManager->create(\Magento\Eav\Setup\EavSetupFactory::class); +/** @var \Magento\Eav\Setup\EavSetup $eavSetup */ +$eavSetup = $eavSetupFactory->create(); +$eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'quantity_and_stock_status', + [ + 'is_used_in_grid' => 1, + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php new file mode 100644 index 0000000000000..fba12f19fdca8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$eavSetupFactory = $objectManager->create(\Magento\Eav\Setup\EavSetupFactory::class); +/** @var \Magento\Eav\Setup\EavSetup $eavSetup */ +$eavSetup = $eavSetupFactory->create(); +$eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'quantity_and_stock_status', + [ + 'is_used_in_grid' => 0, + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php deleted file mode 100644 index cbc0476efd1b5..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -/* Delete attribute with text_attribute code */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); -/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ -$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' -); -$attribute->load('text_attribute', 'attribute_code'); -$attribute->delete(); - -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index b562879b319d6..d3a2e4c53f246 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -55,6 +55,16 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te 'is_salable', // stock indexation is not performed during import ]; + /** + * @var array + */ + private static $attributesToRefresh = [ + 'tax_class_id', + ]; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -65,12 +75,17 @@ protected function setUp() \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::$commonAttributesCache = []; } + /** + * @inheritdoc + */ protected function tearDown() { - $this->executeRollbackFixtures($this->fixtures); + $this->executeFixtures($this->fixtures, true); } /** + * Run import/export tests. + * * @magentoAppArea adminhtml * @magentoDbIsolation disabled * @magentoAppIsolation enabled @@ -78,36 +93,60 @@ protected function tearDown() * @param array $fixtures * @param string[] $skus * @param string[] $skippedAttributes + * @return void * @dataProvider exportImportDataProvider */ - public function testExport($fixtures, $skus, $skippedAttributes = []) + public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void { $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); + $this->executeFixtures($fixtures); $this->modifyData($skus); $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); - $this->executeExportTest($skus, $skippedAttributes); + $csvFile = $this->executeExportTest($skus, $skippedAttributes); + + $this->executeImportReplaceTest($skus, $skippedAttributes, false, $csvFile); + $this->executeImportReplaceTest($skus, $skippedAttributes, true, $csvFile); + $this->executeImportDeleteTest($skus, $csvFile); } - abstract public function exportImportDataProvider(); + /** + * Provide data for import/export. + * + * @return array + */ + abstract public function exportImportDataProvider(): array; /** + * Modify data. + * * @param array $skus + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function modifyData($skus) + protected function modifyData(array $skus): void { } /** + * Prepare product. + * * @param \Magento\Catalog\Model\Product $product + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function prepareProduct($product) + public function prepareProduct(\Magento\Catalog\Model\Product $product): void { } - protected function executeExportTest($skus, $skippedAttributes) + /** + * Execute export test. + * + * @param array $skus + * @param array $skippedAttributes + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + protected function executeExportTest(array $skus, array $skippedAttributes): string { $index = 0; $ids = []; @@ -140,10 +179,23 @@ protected function executeExportTest($skus, $skippedAttributes) $this->assertEqualsSpecificAttributes($origProducts[$index], $newProduct); } + + return $csvfile; } - private function assertEqualsOtherThanSkippedAttributes($expected, $actual, $skippedAttributes) - { + /** + * Assert data equals (ignore skipped attributes). + * + * @param array $expected + * @param array $actual + * @param array $skippedAttributes + * @return void + */ + private function assertEqualsOtherThanSkippedAttributes( + array $expected, + array $actual, + array $skippedAttributes + ): void { foreach ($expected as $key => $value) { if (is_object($value) || in_array($key, $skippedAttributes)) { continue; @@ -158,134 +210,93 @@ private function assertEqualsOtherThanSkippedAttributes($expected, $actual, $ski } /** - * @magentoAppArea adminhtml - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled + * Execute import test with delete behavior. * - * @param array $fixtures - * @param string[] $skus - * @dataProvider exportImportDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param array $skus + * @param string|null $csvFile + * @return void */ - public function testImportDelete($fixtures, $skus, $skippedAttributes = []) - { - $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); - $this->modifyData($skus); - $this->executeImportDeleteTest($skus); - } - - protected function executeImportDeleteTest($skus) + protected function executeImportDeleteTest(array $skus, string $csvFile = null): void { - $csvfile = $this->exportProducts(); - $this->importProducts($csvfile, \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $csvFile = $csvFile ?? $this->exportProducts(); + $this->importProducts($csvFile, \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); foreach ($skus as $sku) { $productId = $this->productResource->getIdBySku($sku); - $product->load($productId); - $this->assertNull($product->getId()); + $this->assertFalse($productId); } } /** - * Execute fixtures + * Execute fixtures. * - * @param array $skus * @param array $fixtures + * @param bool $rollback * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function executeFixtures($fixtures, $skus = []) + protected function executeFixtures(array $fixtures, bool $rollback = false) { foreach ($fixtures as $fixture) { - $fixturePath = $this->fileSystem->getDirectoryRead(DirectoryList::ROOT) - ->getAbsolutePath('/dev/tests/integration/testsuite/' . $fixture); + $fixturePath = $this->resolveFixturePath($fixture, $rollback); include $fixturePath; } } /** - * Execute rollback fixtures + * Resolve fixture path. * - * @param array $fixtures - * @return void + * @param string $fixture + * @param bool $rollback + * @return string */ - private function executeRollbackFixtures($fixtures) + private function resolveFixturePath(string $fixture, bool $rollback = false) { - foreach ($fixtures as $fixture) { - $fixturePath = $this->fileSystem->getDirectoryRead(DirectoryList::ROOT) - ->getAbsolutePath('/dev/tests/integration/testsuite/' . $fixture); + $fixturePath = $this->fileSystem->getDirectoryRead(DirectoryList::ROOT) + ->getAbsolutePath('/dev/tests/integration/testsuite/' . $fixture); + if ($rollback) { $fileInfo = pathinfo($fixturePath); $extension = ''; if (isset($fileInfo['extension'])) { $extension = '.' . $fileInfo['extension']; } - $rollbackfixturePath = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '_rollback' . $extension; - if (file_exists($rollbackfixturePath)) { - include $rollbackfixturePath; - } + $fixturePath = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '_rollback' . $extension; } + + return $fixturePath; } /** + * Assert that specific attributes equal. + * * @param \Magento\Catalog\Model\Product $expectedProduct * @param \Magento\Catalog\Model\Product $actualProduct * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { // check custom options } /** - * @magentoAppArea adminhtml - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled + * Execute import test with replace behavior. * - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - */ - public function testImportReplace($fixtures, $skus, $skippedAttributes = []) - { - $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); - $this->modifyData($skus); - $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); - $this->executeImportReplaceTest($skus, $skippedAttributes); - } - - /** - * @magentoAppArea adminhtml - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled - * - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - */ - public function testImportReplaceWithPagination($fixtures, $skus, $skippedAttributes = []) - { - $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); - $this->modifyData($skus); - $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); - $this->executeImportReplaceTest($skus, $skippedAttributes, true); - } - - /** * @param string[] $skus * @param string[] $skippedAttributes * @param bool $usePagination - * + * @param string|null $csvfile + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagination = false) - { + protected function executeImportReplaceTest( + $skus, + $skippedAttributes, + $usePagination = false, + string $csvfile = null + ) { $replacedAttributes = [ 'row_id', 'entity_id', @@ -293,6 +304,7 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin 'media_gallery' ]; $skippedAttributes = array_merge($replacedAttributes, $skippedAttributes); + $this->cleanAttributesCache(); $index = 0; $ids = []; @@ -316,15 +328,15 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin $itemsPerPageProperty->setValue($exportProduct, 1); } - $csvfile = $this->exportProducts($exportProduct); + $csvfile = $csvfile ?? $this->exportProducts($exportProduct); $this->importProducts($csvfile, \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE); while ($index > 0) { $index--; $newProduct = $productRepository->get($skus[$index], false, Store::DEFAULT_STORE_ID, true); // check original product is deleted - $origProduct = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load($ids[$index]); - $this->assertNull($origProduct->getId()); + $productId = $this->productResource->getIdBySku($ids[$index]); + $this->assertFalse($productId); // check new product data // @todo uncomment or remove after MAGETWO-49806 resolved @@ -342,7 +354,7 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin array_filter($origProductData[$attribute]) : $origProductData[$attribute]; if (!empty($expected)) { - $actual = isset($newProductData[$attribute]) ? $newProductData[$attribute] : null; + $actual = $newProductData[$attribute] ?? null; $actual = is_array($actual) ? array_filter($actual) : $actual; $this->assertNotEquals($expected, $actual, $attribute . ' is expected to be changed'); } @@ -352,7 +364,7 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin } /** - * Export products in the system + * Export products in the system. * * @param \Magento\CatalogImportExport\Model\Export\Product|null $exportProduct * @return string Return exported file name @@ -371,17 +383,18 @@ private function exportProducts(\Magento\CatalogImportExport\Model\Export\Produc ) ); $this->assertNotEmpty($exportProduct->export()); + return $csvfile; } /** - * Import products from the given file + * Import products from the given file. * * @param string $csvfile * @param string $behavior * @return void */ - private function importProducts($csvfile, $behavior) + private function importProducts(string $csvfile, string $behavior): void { /** @var \Magento\CatalogImportExport\Model\Import\Product $importModel */ $importModel = $this->objectManager->create( @@ -437,15 +450,33 @@ private function importProducts($csvfile, $behavior) } /** + * Extract error message. + * * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[] $errors * @return string */ - private function extractErrorMessage($errors) + private function extractErrorMessage(array $errors): string { $errorMessage = ''; foreach ($errors as $error) { $errorMessage = "\n" . $error->getErrorMessage(); } + return $errorMessage; } + + /** + * Clean import attribute cache. + * + * @return void + */ + private function cleanAttributesCache(): void + { + foreach (self::$attributesToRefresh as $attributeCode) { + $attributeId = Import\Product\Type\AbstractType::$attributeCodeToId[$attributeCode] ?? null; + if ($attributeId !== null) { + unset(Import\Product\Type\AbstractType::$commonAttributesCache[$attributeId]); + } + } + } } 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 e5d97ac0e6844..67446960e15dc 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -34,6 +34,7 @@ * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * phpcs:disable Generic.PHP.NoSilencedErrors, Generic.Metrics.NestingLevel, Magento2.Functions.StaticFunction */ class ProductTest extends \Magento\TestFramework\Indexer\TestCase { @@ -567,6 +568,7 @@ public function testSaveDatetimeAttribute() */ protected function getExpectedOptionsData(string $pathToFile, string $storeCode = ''): array { + // phpcs:disable Magento2.Functions.DiscouragedFunction $productData = $this->csvToArray(file_get_contents($pathToFile)); $expectedOptionId = 0; $expectedOptions = []; @@ -1590,6 +1592,28 @@ public function testAddUpdateProductWithInvalidUrlKeys() : void } } + /** + * Make sure the non existing image in the csv file won't erase the qty key of the existing products. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testImportWithNonExistingImage() + { + $products = [ + 'simple_new' => 100, + ]; + + $this->importFile('products_to_import_with_non_existing_image.csv'); + + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + foreach ($products as $productSku => $productQty) { + $product = $productRepository->get($productSku); + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $this->assertEquals($productQty, $stockItem->getQty()); + } + } + /** * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php * @magentoDbIsolation disabled @@ -1781,6 +1805,7 @@ function (ProductInterface $item) { if ($product->getId()) { $productRepository->delete($product); } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { //Product already removed } @@ -2272,6 +2297,20 @@ public function testImportWithBackordersEnabled(): void $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']); } + /** + * Test that imported product stock status with stock quantity > 0 and backorders functionality disabled + * can be set to 'out of stock'. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testImportWithBackordersDisabled(): void + { + $this->importFile('products_to_import_with_backorders_disabled_and_not_0_qty.csv'); + $product = $this->getProductBySku('simple_new'); + $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']); + } + /** * Import file by providing import filename in parameters. * diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv new file mode 100644 index 0000000000000..b22427a8af120 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,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_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,,,,,,,10/20/2015 7:05,10/20/2015 7:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,0,1,1,10000,1,0,1,1,1,0,1,1,0,0,0,1,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_existing_image.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_existing_image.csv new file mode 100644 index 0000000000000..8122433a8c9e1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_existing_image.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,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_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,/no/exists/image/magento_image.jpg,Image Label,magento_small_image.jpg,Small Image Label,magento_thumbnail.jpg,Thumbnail Label,magento_image.jpg,Image Label,10/20/15 07:05,10/20/15 07:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,"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/CatalogImportExport/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php index 11cc73e2cf944..c39acbc338727 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php @@ -10,18 +10,10 @@ */ class ProductTest extends AbstractProductExportImportTestCase { - /** - * Set up - */ - protected function setUp() - { - $this->markTestSkipped('MAGETWO-97378'); - } - /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function exportImportDataProvider() + public function exportImportDataProvider(): array { return [ 'product_export_data' => [ @@ -144,11 +136,6 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - /** * Fixing https://github.com/magento-engcom/import-export-improvements/issues/50 means that during import images * can now get renamed for this we need to skip the attribute checking and instead check that the images contain @@ -158,8 +145,10 @@ public function importReplaceDataProvider() * @param \Magento\Catalog\Model\Product $expectedProduct * @param \Magento\Catalog\Model\Product $actualProduct */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { if (!empty($actualProduct->getImage()) && !empty($expectedProduct->getImage()) ) { diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php index 168073bc6ab74..c57c7c3fd6a92 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php index 168073bc6ab74..c57c7c3fd6a92 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 59ad91ae7b076..56c5db5572a31 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -14,6 +14,9 @@ use Magento\Catalog\Model\Product; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class for testing fulltext index rebuild + */ class FullTest extends \PHPUnit\Framework\TestCase { /** @@ -21,6 +24,9 @@ class FullTest extends \PHPUnit\Framework\TestCase */ protected $actionFull; + /** + * @inheritdoc + */ protected function setUp() { $this->actionFull = Bootstrap::getObjectManager()->create( @@ -29,6 +35,8 @@ protected function setUp() } /** + * Testing fulltext index rebuild + * * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php @@ -39,7 +47,6 @@ public function testGetIndexData() $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); $allowedStatuses = Bootstrap::getObjectManager()->get(Status::class)->getVisibleStatusIds(); $allowedVisibility = Bootstrap::getObjectManager()->get(Engine::class)->getAllowedVisibility(); - $result = iterator_to_array($this->actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); $this->assertNotEmpty($result); @@ -58,7 +65,10 @@ public function testGetIndexData() } /** + * Prepare and return expected index data + * * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getExpectedIndexData() { @@ -68,32 +78,48 @@ private function getExpectedIndexData() $nameId = $attributeRepository->get(ProductInterface::NAME)->getAttributeId(); /** @see dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php */ $configurableId = $attributeRepository->get('test_configurable')->getAttributeId(); + $statusId = $attributeRepository->get(ProductInterface::STATUS)->getAttributeId(); + $taxClassId = $attributeRepository + ->get(\Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID) + ->getAttributeId(); return [ 'configurable' => [ $skuId => 'configurable', $configurableId => 'Option 1 | Option 2', $nameId => 'Configurable Product | Configurable OptionOption 1 | Configurable OptionOption 2', + $taxClassId => 'Taxable Goods | Taxable Goods | Taxable Goods', + $statusId => 'Enabled | Enabled | Enabled' ], 'index_enabled' => [ $skuId => 'index_enabled', $nameId => 'index enabled', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ], 'index_visible_search' => [ $skuId => 'index_visible_search', $nameId => 'index visible search', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ], 'index_visible_category' => [ $skuId => 'index_visible_category', $nameId => 'index visible category', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ], 'index_visible_both' => [ $skuId => 'index_visible_both', $nameId => 'index visible both', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ] ]; } /** + * Testing fulltext index rebuild with configurations + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php */ public function testRebuildStoreIndexConfigurable() @@ -114,6 +140,8 @@ public function testRebuildStoreIndexConfigurable() } /** + * Get product Id by its SKU + * * @param string $sku * @return int */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php new file mode 100644 index 0000000000000..d3f0e9fa2a5ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogUrlRewrite\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class ProcessUrlRewriteOnChangeVisibilityObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ManagerInterface + */ + private $eventManager; + + /** + * Set up + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->eventManager = $this->objectManager->create(ManagerInterface::class); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @magentoAppIsolation enabled + */ + public function testMakeProductInvisibleViaMassAction() + { + /** @var \Magento\Catalog\Model\Product $product*/ + $product = $this->productRepository->get('product1'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(0); + + $testStore = $storeManager->getStore('test'); + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => '1', + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_NOT_VISIBLE ], + 'product_ids' => [$product->getId()] + ] + ); + + $actual = $this->getActualResults($productFilter); + $this->assertCount(0, $actual); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php + * @magentoAppIsolation enabled + */ + public function testMakeProductVisibleViaMassAction() + { + /** @var \Magento\Catalog\Model\Product $product*/ + $product = $this->productRepository->get('product1'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(0); + + $testStore = $storeManager->getStore('test'); + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + $actual = $this->getActualResults($productFilter); + $this->assertCount(0, $actual); + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH ], + 'product_ids' => [$product->getId()] + ] + ); + + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => '1', + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/products_invisible.php + * @magentoAppIsolation enabled + */ + public function testErrorOnDuplicatedUrlKey() + { + $skus = ['product1', 'product2']; + foreach ($skus as $sku) { + /** @var \Magento\Catalog\Model\Product $product */ + $productIds[] = $this->productRepository->get($sku)->getId(); + } + $this->expectException(UrlAlreadyExistsException::class); + $this->expectExceptionMessage('Can not change the visibility of the product with SKU equals "product2". ' + . 'URL key "product-1" for specified store already exists.'); + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH ], + 'product_ids' => $productIds + ] + ); + } + + /** + * @param array $filter + * @return array + */ + private function getActualResults(array $filter) + { + /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $actualResults = []; + foreach ($urlFinder->findAllByData($filter) as $url) { + $actualResults[] = [ + 'request_path' => $url->getRequestPath(), + 'target_path' => $url->getTargetPath(), + 'is_auto_generated' => (int)$url->getIsAutogenerated(), + 'redirect_type' => $url->getRedirectType(), + 'store_id' => $url->getStoreId() + ]; + } + return $actualResults; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php new file mode 100644 index 0000000000000..d50b29383b1ef --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +\Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); + +require __DIR__ . '/../../Store/_files/store.php'; + +/** @var $installer CategorySetup */ +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$storeManager = $objectManager->get(StoreManagerInterface::class); +$storeManager->setCurrentStore(0); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setStoreId(0) + ->setWebsiteIds([1]) + ->setName('Product1') + ->setSku('product1') + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setUrlKey('product-1') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php new file mode 100644 index 0000000000000..bcf399cb5e552 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('product1', true); + if ($product->getId()) { + $productRepository->delete($product); + } +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/store_rollback.php'; +require __DIR__ . '/../../Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php new file mode 100644 index 0000000000000..c72d9c8284db3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +\Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); + +/** @var $installer CategorySetup */ +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$skus = ['product1', 'product2']; +foreach ($skus as $sku) { + /** @var $product \Magento\Catalog\Model\Product */ + $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setStoreId(0) + ->setWebsiteIds([1]) + ->setName('Product1') + ->setSku($sku) + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setUrlKey('product-1') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php new file mode 100644 index 0000000000000..d3d17542aa6ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$skus = ['product1', 'product2']; +foreach ($skus as $sku) { + try { + $product = $productRepository->get($sku, true); + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (NoSuchEntityException $e) { + } +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/store_rollback.php'; +require __DIR__ . '/../../Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php new file mode 100644 index 0000000000000..50b1256c0f124 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +/** + * Test GuestShippingInformationManagement API. + */ +class GuestShippingInformationManagementTest extends TestCase +{ + /** + * @var GuestShippingInformationManagementInterface + */ + private $management; + + /** + * @var CartRepositoryInterface + */ + private $cartRepo; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingFactory; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteria; + + /** + * @var QuoteIdMaskFactory + */ + private $maskFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->management = $objectManager->get(GuestShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + $this->searchCriteria = $objectManager->get(SearchCriteriaBuilder::class); + $this->maskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider getAddressesVariation + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage The shipping information was unable to be saved. Verify the input data and try again. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $carts = $this->cartRepo->getList( + $this->searchCriteria->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + $cart = array_pop($carts); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + /** @var QuoteIdMask $idMask */ + $idMask = $this->maskFactory->create(); + $idMask->load($cart->getId(), 'quote_id'); + $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); + } + + /** + * Different variations for addresses test. + * + * @return array + */ + public function getAddressesVariation(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php new file mode 100644 index 0000000000000..7440fb7fd3d98 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test ShippingInformationManagement API. + */ +class ShippingInformationManagementTest extends TestCase +{ + /** + * @var ShippingInformationManagementInterface + */ + private $management; + + /** + * @var CartRepositoryInterface + */ + private $cartRepo; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->management = $objectManager->get(ShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider getAddressesVariation + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage The shipping information was unable to be saved. Verify the input data and try again. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $cart = $this->cartRepo->getForCustomer(1); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + $this->management->saveAddressInformation($cart->getId(), $shippingInformation); + } + + /** + * Different variations for addresses test. + * + * @return array + */ + public function getAddressesVariation(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php index 3e99c5cad3c39..2ba798e4811ad 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php @@ -39,4 +39,35 @@ public function testExecute() \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } + + /** + * Testing by adding a valid coupon to cart + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoDataFixture Magento/Usps/Fixtures/cart_rule_coupon_free_shipping.php + * @return void + */ + public function testAddingValidCoupon(): void + { + /** @var $session \Magento\Checkout\Model\Session */ + $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + $quote = $session->getQuote(); + $quote->setData('trigger_recollect', 1)->setTotalsCollectedFlag(true); + + $couponCode = 'IMPHBR852R61'; + $inputData = [ + 'remove' => 0, + 'coupon_code' => $couponCode + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($inputData); + $this->dispatch( + 'checkout/cart/couponPost/' + ); + + $this->assertSessionMessages( + $this->equalTo(['You used coupon code "' . $couponCode . '".']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php index 994076badddae..60ccdb88676aa 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php @@ -22,6 +22,7 @@ class ResetQuoteAddressesTest extends \PHPUnit\Framework\TestCase /** * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php * + * @magentoAppArea frontend * @return void */ public function testAfterRemoveItem(): void diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php index 2d948ebeb0128..52437ef828afd 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php @@ -9,3 +9,11 @@ ->setIsMultiShipping(false) ->setReservedOrderId('test_order_1') ->save(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store.php new file mode 100644 index 0000000000000..6448a570424e2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +include 'testsuite/Magento/Store/_files/second_store.php'; +include 'testsuite/Magento/Customer/_files/customer.php'; + +$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); +$quote->setStoreId($store->getId()) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setReservedOrderId('test_order_1_not_default_store') + ->setCustomerId($customer->getId()) + ->save(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php new file mode 100644 index 0000000000000..e3e1513cb6144 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->load('test_order_1_not_default_store', 'reserved_order_id')->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store.php new file mode 100644 index 0000000000000..bbd3d5efbe8c8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +include 'testsuite/Magento/Store/_files/second_store.php'; + +$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); +$quote->setStoreId($store->getId()) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setReservedOrderId('test_order_1_not_default_store_guest') + ->save(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store_rollback.php new file mode 100644 index 0000000000000..f511133280e7f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->load('test_order_1_not_default_store_guest', 'reserved_order_id')->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php index 507f6b755bcda..e66227a60e8f0 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php @@ -21,7 +21,7 @@ ], 'customer_group_ids' => [1], 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC, - 'coupon_code' => uniqid(), + 'coupon_code' => '2?ds5!2d', 'simple_action' => \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION, 'discount_amount' => 10, 'discount_step' => 1 diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php index 3c54fe16db7d3..61779da29c65f 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php @@ -11,10 +11,18 @@ require 'quote_with_address_saved.php'; +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$rate = $objectManager->get(\Magento\Quote\Model\Quote\Address\Rate::class); + $quote->load('test_order_1', 'reserved_order_id'); $shippingAddress = $quote->getShippingAddress(); $shippingAddress->setShippingMethod('flatrate_flatrate') ->setShippingDescription('Flat Rate - Fixed') - ->setShippingAmount(10.0) - ->setBaseShippingAmount(12.0) ->save(); + +$rate->setPrice(0) + ->setAddressId($shippingAddress->getId()) + ->save(); +$shippingAddress->setBaseShippingAmount($rate->getPrice()); +$shippingAddress->setShippingAmount($rate->getPrice()); +$rate->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php index 105a981ccfc84..39b758447221c 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php @@ -14,3 +14,5 @@ /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); + +require 'simple_product_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php index 2b906bdc022f5..402ad030ed857 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php @@ -13,3 +13,7 @@ /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); + +require __DIR__ . '/../../Customer/_files/customer_rollback.php'; +require __DIR__ . '/../../Customer/_files/customer_address_rollback.php'; +require __DIR__ . '/../../Catalog/_files/product_virtual_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php index 835b2ab812856..833e5a57ac34f 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php @@ -13,6 +13,7 @@ ->setIsMultiShipping(false) ->setReservedOrderId('test_order_with_virtual_product_without_address') ->setEmail('store@example.com') + ->setCustomerEmail('store@example.com') ->addProduct( $product->load($product->getId()), 1 diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php index b3224bb527442..afcb7f56f8d1b 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php @@ -14,3 +14,5 @@ /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_virtual_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/simple_product_rollback.php new file mode 100644 index 0000000000000..d8744873af00c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/simple_product_rollback.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php index a1a29706756b5..c574869a83cab 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php @@ -66,6 +66,7 @@ public function testExecute() $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $directoryName) ); $this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]); + $this->model->getRequest()->setMethod('POST'); $this->model->execute(); $this->assertFalse( diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php index bab14a8663eae..00f56e5700415 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php @@ -4,9 +4,14 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Controller\Result\Json as JsonResponse; +use Magento\Framework\App\Response\HttpFactory as ResponseFactory; +use Magento\Framework\App\Response\Http as Response; /** * Test for \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload class. @@ -43,6 +48,11 @@ class UploadTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @var HttpFactory + */ + private $responseFactory; + /** * @inheritdoc */ @@ -56,6 +66,7 @@ protected function setUp() $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fullDirectoryPath = $imagesHelper->getStorageRoot() . DIRECTORY_SEPARATOR . $directoryName; $this->mediaDirectory->create($this->mediaDirectory->getRelativePath($this->fullDirectoryPath)); + $this->responseFactory = $this->objectManager->get(ResponseFactory::class); $this->model = $this->objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload::class); $fixtureDir = realpath(__DIR__ . '/../../../../../Catalog/_files'); $tmpFile = $this->filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath() . $this->fileName; @@ -81,8 +92,13 @@ protected function setUp() public function testExecute() { $this->model->getRequest()->setParams(['type' => 'image/png']); + $this->model->getRequest()->setMethod('POST'); $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); - $this->model->execute(); + /** @var JsonResponse $jsonResponse */ + $jsonResponse = $this->model->execute(); + /** @var Response $response */ + $jsonResponse->renderResult($response = $this->responseFactory->create()); + $data = json_decode($response->getBody(), true); $this->assertTrue( $this->mediaDirectory->isExist( @@ -91,6 +107,12 @@ public function testExecute() ) ) ); + //Asserting that response contains only data needed by clients. + $keys = ['name', 'type', 'error', 'size', 'file']; + sort($keys); + $dataKeys = array_keys($data); + sort($dataKeys); + $this->assertEquals($keys, $dataKeys); } /** diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php index 73f1dd9e7b711..e59672f1b5e1a 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php @@ -7,6 +7,8 @@ namespace Magento\Config\Console\Command; use Magento\Config\Model\Config\Backend\Admin\Custom; +use Magento\Config\Model\Config\Structure\Converter; +use Magento\Config\Model\Config\Structure\Data as StructureData; use Magento\Directory\Model\Currency; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -91,6 +93,8 @@ protected function setUp() { Bootstrap::getInstance()->reinitialize(); $this->objectManager = Bootstrap::getObjectManager(); + $this->extendSystemStructure(); + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); $this->reader = $this->objectManager->get(FileReader::class); $this->filesystem = $this->objectManager->get(Filesystem::class); @@ -123,6 +127,21 @@ protected function tearDown() $this->appConfig->reinit(); } + /** + * Add test system structure to main system structure + * + * @return void + */ + private function extendSystemStructure() + { + $document = new \DOMDocument(); + $document->load(__DIR__ . '/../../_files/system.xml'); + $converter = $this->objectManager->get(Converter::class); + $systemConfig = $converter->convert($document); + $structureData = $this->objectManager->get(StructureData::class); + $structureData->merge($systemConfig); + } + /** * @return array */ @@ -191,6 +210,8 @@ public function runLockDataProvider() ['general/region/display_all', '1'], ['general/region/state_required', 'BR,FR', ScopeInterface::SCOPE_WEBSITE, 'base'], ['admin/security/use_form_key', '0'], + ['general/group/subgroup/field', 'default_value'], + ['general/group/subgroup/field', 'website_value', ScopeInterface::SCOPE_WEBSITE, 'base'], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ConverterStub.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ConverterStub.php new file mode 100644 index 0000000000000..7493d31f02b31 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ConverterStub.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\Reader; + +use Magento\Config\Model\Config\Structure\Converter; + +/** + * Class ConverterStub used for ReaderTest. + */ +class ConverterStub extends Converter +{ + /** + * Convert dom document wrapper. + * + * @param \DOMDocument $document + * @return array|null + */ + public function getArrayData(\DOMDocument $document) + { + return $this->_convertDOMDocument($document); + } + + /** + * Convert dom document. + * + * @param \DOMNode $source + * @return array + */ + public function convert($source) + { + return $this->_convertDOMDocument($source); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderStub.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderStub.php new file mode 100644 index 0000000000000..866ff91678ec4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderStub.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\Reader; + +use Magento\Config\Model\Config\Structure\Reader; + +/** + * Class ReaderStub used for testing protected Reader::_readFiles() method. + */ +class ReaderStub extends Reader +{ + /** + * Wrapper for protected Reader::_readFiles() method. + * + * @param array $fileList + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function readFiles(array $fileList) + { + return $this->_readFiles($fileList); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderTest.php new file mode 100644 index 0000000000000..eef8e68458d91 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\Reader; + +use Magento\Config\Model\Config\SchemaLocator; +use Magento\Framework\App\Utility\Files; +use Magento\Framework\Config\Dom; +use Magento\Framework\Config\FileResolverInterface; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\TemplateEngine\Xhtml\CompilerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class ReaderTest check Magento\Config\Model\Config\Structure\Reader::_readFiles() method. + */ +class ReaderTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test config location. + * + * @string + */ + const CONFIG = '/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Files + */ + private $fileUtility; + + /** + * @var ValidationStateInterface + */ + private $validationStateMock; + + /** + * @var \Magento\Framework\Config\SchemaLocatorInterface + */ + private $schemaLocatorMock; + + /** + * @var FileResolverInterface + */ + private $fileResolverMock; + + /** + * @var ReaderStub + */ + private $reader; + + /** + * @var ConverterStub + */ + private $converter; + + /** + * @var CompilerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $compiler; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->fileUtility = Files::init(); + + $this->validationStateMock = $this->getMockBuilder(ValidationStateInterface::class) + ->setMethods(['isValidationRequired']) + ->getMockForAbstractClass(); + $this->schemaLocatorMock = $this->getMockBuilder(SchemaLocator::class) + ->disableOriginalConstructor() + ->setMethods(['getPerFileSchema']) + ->getMock(); + $this->fileResolverMock = $this->getMockBuilder(FileResolverInterface::class) + ->getMockForAbstractClass(); + + $this->validationStateMock->expects($this->atLeastOnce()) + ->method('isValidationRequired') + ->willReturn(false); + $this->schemaLocatorMock->expects($this->atLeastOnce()) + ->method('getPerFileSchema') + ->willReturn(false); + + $this->converter = $this->objectManager->create(ConverterStub::class); + + //Isolate test from actual configuration, and leave only sample data. + $this->compiler = $this->getMockBuilder(CompilerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['compile']) + ->getMockForAbstractClass(); + + $this->reader = $this->objectManager->create( + ReaderStub::class, + [ + 'fileResolver' => $this->fileResolverMock, + 'converter' => $this->converter, + 'schemaLocator' => $this->schemaLocatorMock, + 'validationState' => $this->validationStateMock, + 'fileName' => 'no_existing_file.xml', + 'compiler' => $this->compiler, + 'domDocumentClass' => Dom::class + ] + ); + } + + /** + * The test checks the file structure after processing the nodes responsible for inserting content. + * + * @return void + */ + public function testXmlConvertedConfigurationAndCompereStructure() + { + $actual = $this->reader->readFiles(['actual' => $this->getContent()]); + + $document = new \DOMDocument(); + $document->loadXML($this->getContent()); + + $expected = $this->converter->getArrayData($document); + + $this->assertEquals($expected, $actual); + } + + /** + * Get config sample data for test. + * + * @return string + */ + protected function getContent() + { + $files = $this->fileUtility->getFiles([BP . static::CONFIG], 'config.xml'); + + return file_get_contents(reset($files)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/actual/config.xml b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml similarity index 100% rename from dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/actual/config.xml rename to dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/system.xml b/dev/tests/integration/testsuite/Magento/Config/_files/system.xml new file mode 100644 index 0000000000000..f0063a3c0bf7f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/_files/system.xml @@ -0,0 +1,20 @@ +<?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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="general"> + <group id="group"> + <group id="subgroup"> + <field id="field" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Label</label> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php index 5184a37563317..338daa56450d4 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php @@ -9,7 +9,10 @@ class ConfigurableTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ 'configurable-product' => [ @@ -34,11 +37,12 @@ public function exportImportDataProvider() } /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable $productType */ $productType = $expectedProduct->getTypeInstance(); $expectedAssociatedProducts = $productType->getUsedProductCollection($expectedProduct); @@ -95,12 +99,16 @@ protected function assertEqualsSpecificAttributes($expectedProduct, $actualProdu } } - public function importReplaceDataProvider() - { - $data = $this->exportImportDataProvider(); - foreach ($data as $key => $value) { - $data[$key][2] = array_merge($value[2], ['_cache_instance_product_set_attributes']); - } - return $data; + /** + * @inheritdoc + */ + protected function executeImportReplaceTest( + $skus, + $skippedAttributes, + $usePagination = false, + string $csvfile = null + ) { + $skippedAttributes = array_merge($skippedAttributes, ['_cache_instance_product_set_attributes']); + parent::executeImportReplaceTest($skus, $skippedAttributes, $usePagination, $csvfile); } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php new file mode 100644 index 0000000000000..1fffd701c509f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use PHPUnit\Framework\TestCase; + +/** + * Test configurable fronted product plugin will add children products ids to configurable product identities. + */ +class ProductIdentitiesExtenderTest extends TestCase +{ + /** + * Check, product identities extender plugin is registered for storefront. + * + * @magentoAppArea frontend + * @return void + */ + public function testIdentitiesExtenderIsRegistered(): void + { + $pluginInfo = Bootstrap::getObjectManager()->get(PluginList::class) + ->get(\Magento\Catalog\Model\Product::class, []); + $this->assertSame(ProductIdentitiesExtender::class, $pluginInfo['product_identities_extender']['instance']); + } + + /** + * Check plugin will add children ids to configurable product identities on storefront. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea frontend + * @return void + */ + public function testGetIdentitiesForConfigurableProductOnStorefront(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->get('configurable'); + $simpleProduct1 = $productRepository->get('simple_10'); + $simpleProduct2 = $productRepository->get('simple_20'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + 'cat_p', + 'cat_p_' . $simpleProduct1->getId(), + 'cat_p_' . $simpleProduct2->getId(), + + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); + } + + /** + * Check plugin won't add children ids to configurable product identities in admin area. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + * @return void + */ + public function testGetIdentitiesForConfigurableProductInAdminArea(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->get('configurable'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php index 234f0aae6a3ea..676433c0a1e6c 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php @@ -5,7 +5,19 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\Data; -class AssociatedProductsTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\ConfigurablePanel; +use Magento\Framework\App\RequestInterface; +use PHPUnit\Framework\TestCase; + +/** + * AssociatedProductsTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AssociatedProductsTest extends TestCase { /** * @var \Magento\Framework\ObjectManagerInterface $objectManager @@ -17,7 +29,10 @@ class AssociatedProductsTest extends \PHPUnit\Framework\TestCase */ private $registry; - public function setUp() + /** + * @inheritdoc + */ + public function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->registry = $this->objectManager->get(\Magento\Framework\Registry::class); @@ -64,6 +79,53 @@ public function testGetProductMatrix($interfaceLocale) } } + /** + * Tests configurable product won't appear in product listing. + * + * Tests configurable product won't appear in configurable associated product listing if its options attribute + * is not filterable in grid. + * + * @return void + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + */ + public function testAddManuallyConfigurationsWithNotFilterableInGridAttribute(): void + { + /** @var RequestInterface $request */ + $request = $this->objectManager->get(RequestInterface::class); + $request->setParams([ + FilterModifier::FILTER_MODIFIER => [ + 'test_configurable' => [ + 'condition_type' => 'notnull', + ], + ], + 'attributes_codes' => [ + 'test_configurable' + ], + ]); + $context = $this->objectManager->create(ContextInterface::class, ['request' => $request]); + /** @var UiComponentFactory $uiComponentFactory */ + $uiComponentFactory = $this->objectManager->get(UiComponentFactory::class); + $uiComponent = $uiComponentFactory->create( + ConfigurablePanel::ASSOCIATED_PRODUCT_LISTING, + null, + ['context' => $context] + ); + + foreach ($uiComponent->getChildComponents() as $childUiComponent) { + $childUiComponent->prepare(); + } + + $dataSource = $uiComponent->getDataSourceData(); + $skus = array_column($dataSource['data']['items'], 'sku'); + + $this->assertNotContains( + 'configurable', + $skus, + 'Only products with specified attribute should be in product list' + ); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php new file mode 100644 index 0000000000000..69607ffb445ba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Eav\Model\Entity\Attribute\FrontendLabel; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/configurable_products.php'; + +// Add frontend label to created attribute: +$frontendLabelAttribute = Bootstrap::getObjectManager()->get(FrontendLabel::class); +$frontendLabelAttribute->setStoreId(1); +$frontendLabelAttribute->setLabel('Default Store View label'); + +$frontendLabels = $attribute->getFrontendLabels(); +$frontendLabels[] = $frontendLabelAttribute; + +$attribute->setFrontendLabels($frontendLabels); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute_rollback.php new file mode 100644 index 0000000000000..616bd44666efc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/configurable_products_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php index 4c31fb740c57e..1ef7d54c5aa78 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php @@ -65,6 +65,7 @@ public function testGetAddressEditUrl() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider hasPrimaryAddressDataProvider + * @magentoAppIsolation enabled */ public function testHasPrimaryAddress($customerId, $expected) { @@ -82,6 +83,7 @@ public function hasPrimaryAddressDataProvider() /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoAppIsolation enabled */ public function testGetAdditionalAddresses() { @@ -98,6 +100,7 @@ public function testGetAdditionalAddresses() /** * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider getAdditionalAddressesDataProvider + * @magentoAppIsolation enabled */ public function testGetAdditionalAddressesNegative($customerId, $expected) { @@ -115,6 +118,7 @@ public function getAdditionalAddressesDataProvider() /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @magentoAppIsolation enabled */ public function testGetAddressHtml() { @@ -134,6 +138,7 @@ public function testGetAddressHtmlWithoutAddress() /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppIsolation enabled */ public function testGetCustomer() { @@ -158,6 +163,7 @@ public function testGetCustomerMissingCustomer() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider getDefaultBillingDataProvider + * @magentoAppIsolation enabled */ public function testGetDefaultBilling($customerId, $expected) { @@ -175,6 +181,7 @@ public function getDefaultBillingDataProvider() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider getDefaultShippingDataProvider + * @magentoAppIsolation enabled */ public function testGetDefaultShipping($customerId, $expected) { diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/GridTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/GridTest.php new file mode 100644 index 0000000000000..ac11c6c08bd62 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/GridTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Block\Address; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Integration tests for the \Magento\Customer\Block\Address\Grid class + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\View\LayoutInterface + */ + private $layout; + + /** + * @var \Magento\Customer\Helper\Session\CurrentCustomer + */ + protected $currentCustomer; + + protected function setUp() + { + /** @var \PHPUnit_Framework_MockObject_MockObject $blockMock */ + $blockMock = $this->getMockBuilder( + \Magento\Framework\View\Element\BlockInterface::class + )->disableOriginalConstructor()->setMethods( + ['setTitle', 'toHtml'] + )->getMock(); + $blockMock->expects($this->any())->method('setTitle'); + + $this->currentCustomer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Customer\Helper\Session\CurrentCustomer::class); + $this->layout = Bootstrap::getObjectManager()->get(\Magento\Framework\View\LayoutInterface::class); + $this->layout->setBlock('head', $blockMock); + } + + protected function tearDown() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ + $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); + // Cleanup customer from registry + $customerRegistry->remove(1); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppIsolation enabled + */ + public function testGetAddressEditUrl() + { + $gridBlock = $this->createBlockForCustomer(1); + + $this->assertEquals( + 'http://localhost/index.php/customer/address/edit/id/1/', + $gridBlock->getAddressEditUrl(1) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoAppIsolation enabled + */ + public function testGetAdditionalAddresses() + { + $gridBlock = $this->createBlockForCustomer(1); + $this->assertNotNull($gridBlock->getAdditionalAddresses()); + $this->assertCount(1, $gridBlock->getAdditionalAddresses()); + $this->assertInstanceOf( + \Magento\Customer\Api\Data\AddressInterface::class, + $gridBlock->getAdditionalAddresses()[0] + ); + $this->assertEquals(2, $gridBlock->getAdditionalAddresses()[0]->getId()); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @dataProvider getAdditionalAddressesDataProvider + * @magentoAppIsolation enabled + */ + public function testGetAdditionalAddressesNegative($customerId, $expected) + { + $gridBlock = $this->createBlockForCustomer($customerId); + $this->currentCustomer->setCustomerId($customerId); + $this->assertEquals($expected, $gridBlock->getAdditionalAddresses()); + } + + public function getAdditionalAddressesDataProvider() + { + return ['5' => [5, []]]; + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoAppIsolation enabled + */ + public function testGetAddressHtmlWithoutAddress() + { + $gridBlock = $this->createBlockForCustomer(5); + $this->assertEquals('', $gridBlock->getAddressHtml(null)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppIsolation enabled + */ + public function testGetCustomer() + { + $gridBlock = $this->createBlockForCustomer(1); + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = Bootstrap::getObjectManager()->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $customer = $customerRepository->getById(1); + $object = $gridBlock->getCustomer(); + $this->assertEquals($customer, $object); + } + + /** + * Create address book block for customer + * + * @param int $customerId + * @return \Magento\Framework\View\Element\BlockInterface + */ + private function createBlockForCustomer($customerId) + { + $this->currentCustomer->setCustomerId($customerId); + return $this->layout->createBlock( + \Magento\Customer\Block\Address\Grid::class, + '', + ['currentCustomer' => $this->currentCustomer] + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php index 3bb10baff6572..3ade17d90fe99 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View; use Magento\Customer\Controller\RegistryConstants; @@ -100,15 +101,4 @@ public function testToHtmlCartItem() $this->assertContains('$10.00', $html); $this->assertContains('catalog/product/edit/id/1', $html); } - - /** - * Verify that the customer has a single item in his cart. - * - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/quote.php - */ - public function testGetCollection() - { - $this->assertEquals(1, $this->block->getCollection()->getSize()); - } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php index 2cc2c2a426d12..3883f3f88ee5e 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php @@ -15,6 +15,9 @@ class GenderTest extends \PHPUnit\Framework\TestCase /** @var Gender */ protected $_block; + /** @var \Magento\Customer\Model\Attribute */ + private $_model; + /** * Test initialization and set up. Create the Gender block. * @return void @@ -28,6 +31,8 @@ protected function setUp() )->createBlock( \Magento\Customer\Block\Widget\Gender::class ); + $this->_model = $objectManager->create(\Magento\Customer\Model\Attribute::class); + $this->_model->loadByCode('customer', 'gender'); } /** @@ -49,7 +54,8 @@ public function testGetGenderOptions() public function testToHtml() { $html = $this->_block->toHtml(); - $this->assertContains('<span>Gender</span>', $html); + $attributeLabel = $this->_model->getStoreLabel(); + $this->assertContains('<span>' . $attributeLabel . '</span>', $html); $this->assertContains('<option value="1">Male</option>', $html); $this->assertContains('<option value="2">Female</option>', $html); $this->assertContains('<option value="3">Not Specified</option>', $html); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php index 3650a7e95a36c..3bc9fea5db381 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php @@ -22,7 +22,13 @@ public function testToHtml() \Magento\Customer\Block\Widget\Taxvat::class ); - $this->assertContains('title="Tax/VAT number"', $block->toHtml()); + $model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Customer\Model\Attribute::class + ); + $model->loadByCode('customer', 'taxvat'); + $attributeLabel = $model->getStoreLabel(); + + $this->assertContains('title="' . $block->escapeHtmlAttr($attributeLabel) . '"', $block->toHtml()); $this->assertNotContains('required', $block->toHtml()); } @@ -38,13 +44,14 @@ public function testToHtmlRequired() ); $model->loadByCode('customer', 'taxvat')->setIsRequired(true); $model->save(); + $attributeLabel = $model->getStoreLabel(); /** @var \Magento\Customer\Block\Widget\Taxvat $block */ $block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Customer\Block\Widget\Taxvat::class ); - $this->assertContains('title="Tax/VAT number"', $block->toHtml()); + $this->assertContains('title="' . $block->escapeHtmlAttr($attributeLabel) . '"', $block->toHtml()); $this->assertContains('required', $block->toHtml()); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index c94948e23ab4d..10b632c002475 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -17,18 +17,35 @@ use Magento\Framework\App\Http; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Message\MessageInterface; -use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Request; use Magento\TestFramework\Response; use Zend\Stdlib\Parameters; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Theme\Controller\Result\MessagePlugin; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountTest extends \Magento\TestFramework\TestCase\AbstractController { + /** + * @var TransportBuilderMock + */ + private $transportBuilderMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + } + /** * Login the user * @@ -133,11 +150,7 @@ public function testForgotPasswordEmailMessageWithSpecialCharacters() $this->dispatch('customer/account/forgotPasswordPost'); $this->assertRedirect($this->stringContains('customer/account/')); - /** @var \Magento\TestFramework\Mail\Template\TransportBuilderMock $transportBuilder */ - $transportBuilder = $this->_objectManager->get( - \Magento\TestFramework\Mail\Template\TransportBuilderMock::class - ); - $subject = $transportBuilder->getSentMessage()->getSubject(); + $subject = $this->transportBuilderMock->getSentMessage()->getSubject(); $this->assertContains( 'Test special\' characters', $subject @@ -260,26 +273,10 @@ public function testNoFormKeyCreatePostAction() /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_disable.php */ public function testNoConfirmCreatePostAction() { - /** @var \Magento\Framework\App\Config\MutableScopeConfigInterface $mutableScopeConfig */ - $mutableScopeConfig = Bootstrap::getObjectManager() - ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); - - $scopeValue = $mutableScopeConfig->getValue( - 'customer/create_account/confirm', - ScopeInterface::SCOPE_WEBSITES, - null - ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - 0, - ScopeInterface::SCOPE_WEBSITES, - null - ); - $this->fillRequestWithAccountDataAndFormKey('test1@email.com'); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringEndsWith('customer/account/')); @@ -287,38 +284,15 @@ public function testNoConfirmCreatePostAction() $this->equalTo(['Thank you for registering with Main Website Store.']), MessageInterface::TYPE_SUCCESS ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - $scopeValue, - ScopeInterface::SCOPE_WEBSITES, - null - ); } /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php */ public function testWithConfirmCreatePostAction() { - /** @var \Magento\Framework\App\Config\MutableScopeConfigInterface $mutableScopeConfig */ - $mutableScopeConfig = Bootstrap::getObjectManager() - ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); - - $scopeValue = $mutableScopeConfig->getValue( - 'customer/create_account/confirm', - ScopeInterface::SCOPE_WEBSITES, - null - ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - 1, - ScopeInterface::SCOPE_WEBSITES, - null - ); - $this->fillRequestWithAccountDataAndFormKey('test2@email.com'); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringContains('customer/account/index/')); @@ -330,13 +304,6 @@ public function testWithConfirmCreatePostAction() ]), MessageInterface::TYPE_SUCCESS ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - $scopeValue, - ScopeInterface::SCOPE_WEBSITES, - null - ); } /** @@ -730,6 +697,46 @@ public function testLoginPostRedirect($redirectDashboard, string $redirectUrl) $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); } + /** + * Test that confirmation email address displays special characters correctly. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php + * + * @return void + */ + public function testConfirmationEmailWithSpecialCharacters(): void + { + $email = 'customer+confirmation@example.com'; + $this->dispatch('customer/account/confirmation/email/customer%2Bconfirmation%40email.com'); + $this->getRequest()->setPostValue('email', $email); + $this->dispatch('customer/account/confirmation/email/customer%2Bconfirmation%40email.com'); + + $this->assertRedirect($this->stringContains('customer/account/index')); + $this->assertSessionMessages( + $this->equalTo(['Please check your email for confirmation key.']), + MessageInterface::TYPE_SUCCESS + ); + + /** @var $message \Magento\Framework\Mail\Message */ + $message = $this->transportBuilderMock->getSentMessage(); + $rawMessage = $message->getRawMessage(); + + $this->assertContains('To: ' . $email, $rawMessage); + + $content = $message->getBody()->getPartContent(0); + $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); + $this->setRequestInfo($confirmationUrl, 'confirm'); + $this->clearCookieMessagesList(); + $this->dispatch($confirmationUrl); + + $this->assertRedirect($this->stringContains('customer/account/index')); + $this->assertSessionMessages( + $this->equalTo(['Thank you for registering with Main Website Store.']), + MessageInterface::TYPE_SUCCESS + ); + } + /** * Data provider for testLoginPostRedirect. * @@ -744,6 +751,21 @@ public function loginPostRedirectDataProvider() ]; } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @magentoAppArea frontend + */ + public function testCheckVisitorModel() + { + /** @var \Magento\Customer\Model\Visitor $visitor */ + $visitor = $this->_objectManager->get(\Magento\Customer\Model\Visitor::class); + $this->login(1); + $this->assertNull($visitor->getId()); + $this->dispatch('customer/account/index'); + $this->assertNotNull($visitor->getId()); + } + /** * @param string $email * @return void @@ -847,4 +869,53 @@ private function assertResponseRedirect(Response $response, string $redirectUrl) $this->assertTrue($response->isRedirect()); $this->assertSame($redirectUrl, $response->getHeader('Location')->getUri()); } + + /** + * Add new request info (request uri, path info, action name). + * + * @param string $uri + * @param string $actionName + * @return void + */ + private function setRequestInfo(string $uri, string $actionName): void + { + $this->getRequest() + ->setRequestUri($uri) + ->setPathInfo() + ->setActionName($actionName); + } + + /** + * Clear cookie messages list. + * + * @return void + */ + private function clearCookieMessagesList(): void + { + $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $jsonSerializer = $this->_objectManager->get(Json::class); + $cookieManager->setPublicCookie( + MessagePlugin::MESSAGES_COOKIES_NAME, + $jsonSerializer->serialize([]) + ); + } + + /** + * Get confirmation URL from message content. + * + * @param string $content + * @return string + */ + private function getConfirmationUrlFromMessageContent(string $content): string + { + $confirmationUrl = ''; + + if (preg_match('<a\s*href="(?<url>.*?)".*>', $content, $matches)) { + $confirmationUrl = $matches['url']; + $confirmationUrl = str_replace('http://localhost/index.php/', '', $confirmationUrl); + $confirmationUrl = html_entity_decode($confirmationUrl); + } + + return $confirmationUrl; + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index 292d61c392d06..1b7f2c1f7efdd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -296,6 +296,51 @@ public function testSaveActionCoreException() $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new/key/')); } + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + */ + public function testSaveActionCoreExceptionFormatFormData() + { + $post = [ + 'customer' => [ + 'middlename' => 'test middlename', + 'website_id' => 1, + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'email' => 'customer@example.com', + 'dob' => '12/3/1996', + ], + ]; + $postCustomerFormatted = [ + 'middlename' => 'test middlename', + 'website_id' => 1, + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'email' => 'customer@example.com', + 'dob' => '1996-12-03', + ]; + + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/customer/index/save'); + /* + * Check that error message is set + */ + $this->assertSessionMessages( + $this->equalTo(['A customer with the same email address already exists in an associated website.']), + \Magento\Framework\Message\MessageInterface::TYPE_ERROR + ); + + $customerFormData = Bootstrap::getObjectManager() + ->get(\Magento\Backend\Model\Session::class) + ->getCustomerFormData(); + $this->assertEquals( + $postCustomerFormatted, + $customerFormData['customer'], + 'Customer form data should be formatted' + ); + $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new/key/')); + } + /** * @magentoDataFixture Magento/Customer/_files/customer_sample.php */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php new file mode 100644 index 0000000000000..54dbdf25dd645 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Controller; + +use Magento\TestFramework\TestCase\AbstractController; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Framework\Data\Form\FormKey; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Model\Session; +use Psr\Log\LoggerInterface; + +class SendTest extends AbstractController +{ + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var FormKey */ + private $formKey; + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function setUp() + { + parent::setUp(); + $logger = $this->createMock(LoggerInterface::class); + $session = Bootstrap::getObjectManager()->create( + Session::class, + [$logger] + ); + $this->accountManagement = Bootstrap::getObjectManager()->create(AccountManagementInterface::class); + $this->formKey = Bootstrap::getObjectManager()->create(FormKey::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecutePost() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com' + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/index')); + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } + /** + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture default_store customer/captcha/forms user_forgotpassword,user_login,share_wishlist_form + * + */ + public function testCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com', + 'captcha' => [ + 'share_wishlist_form' => 'wrong_captcha_word' + ] + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/share')); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + \Magento\Framework\Message\MessageInterface::TYPE_ERROR + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php index 017532fb392b5..b6e8cba82adae 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php @@ -67,4 +67,28 @@ public function testUpdateDataOverrideExistingData() $this->assertEquals('CompanyZ', $updatedAddressData->getCompany()); $this->assertEquals('99999', $updatedAddressData->getPostcode()); } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + */ + public function testUpdateDataForExistingCustomer() + { + /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ + $customerRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(CustomerRegistry::class); + /** @var \Magento\Customer\Model\Data\Address $addressData */ + $updatedAddressData = $this->addressFactory->create() + ->setId(1) + ->setCustomerId($customerRegistry->retrieveByEmail('customer@example.com')->getId()) + ->setCity('CityZ') + ->setCompany('CompanyZ') + ->setPostcode('99999'); + $updatedAddressData = $this->addressModel->updateData($updatedAddressData)->getDataModel(); + + $this->assertEquals(1, $updatedAddressData->getId()); + $this->assertEquals('CityZ', $updatedAddressData->getCity()); + $this->assertEquals('CompanyZ', $updatedAddressData->getCompany()); + $this->assertEquals('99999', $updatedAddressData->getPostcode()); + $this->assertEquals(true, $updatedAddressData->isDefaultBilling()); + $this->assertEquals(true, $updatedAddressData->isDefaultShipping()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php index 794fce17480fa..a5c69bcd3239e 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php @@ -239,10 +239,10 @@ public function testGetCustomerAttributeMetadata() $this->assertNotEmpty($attributes); // remove odd extension attributes - $allAtrributes = $expectAttrsWithVals; - $allAtrributes['created_at'] = $attributes['created_at']; - $allAtrributes['updated_at'] = $attributes['updated_at']; - $attributes = array_intersect_key($attributes, $allAtrributes); + $allAttributes = $expectAttrsWithVals; + $allAttributes['created_at'] = $attributes['created_at']; + $allAttributes['updated_at'] = $attributes['updated_at']; + $attributes = array_intersect_key($attributes, $allAttributes); foreach ($attributes as $attributeCode => $attributeValue) { $this->assertNotNull($attributeCode); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php index 4177698389850..381c580f55e60 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php @@ -17,6 +17,8 @@ use Magento\Store\Api\WebsiteRepositoryInterface; /** + * Class with integration tests for AddressRepository. + * * @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -38,6 +40,9 @@ class AddressRepositoryTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\Api\DataObjectHelper */ private $dataObjectHelper; + /** + * Set up. + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -86,6 +91,9 @@ protected function setUp() $this->expectedAddresses = [$address, $address2]; } + /** + * Tear down. + */ protected function tearDown() { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -95,6 +103,8 @@ protected function tearDown() } /** + * Test for save address changes. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -116,6 +126,8 @@ public function testSaveAddressChanges() } /** + * Test for method save address with new id. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -130,6 +142,8 @@ public function testSaveAddressesIdSetButNotAlreadyExisting() } /** + * Test for method get address by id. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -143,6 +157,8 @@ public function testGetAddressById() } /** + * Test for method get address by id with incorrect id. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @expectedException \Magento\Framework\Exception\NoSuchEntityException * @expectedExceptionMessage No such entity with addressId = 12345 @@ -153,6 +169,8 @@ public function testGetAddressByIdBadAddressId() } /** + * Test for method save new address. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoAppIsolation enabled @@ -179,6 +197,8 @@ public function testSaveNewAddress() } /** + * Test for method saaveNewAddress with new attributes. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoAppIsolation enabled @@ -204,6 +224,8 @@ public function testSaveNewAddressWithAttributes() } /** + * Test for saving address with invalid address. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoAppIsolation enabled @@ -227,6 +249,11 @@ public function testSaveNewInvalidAddress() } } + /** + * Test for saving address without existing customer. + * + * @return void + */ public function testSaveAddressesCustomerIdNotExist() { $proposedAddress = $this->_createSecondAddress()->setCustomerId(4200); @@ -238,6 +265,11 @@ public function testSaveAddressesCustomerIdNotExist() } } + /** + * Test for saving addresses with invalid customer id. + * + * @return void + */ public function testSaveAddressesCustomerIdInvalid() { $proposedAddress = $this->_createSecondAddress()->setCustomerId('this_is_not_a_valid_id'); @@ -250,6 +282,8 @@ public function testSaveAddressesCustomerIdInvalid() } /** + * Test for delete method. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php */ @@ -273,6 +307,8 @@ public function testDeleteAddress() } /** + * Test for deleteAddressById. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php */ @@ -296,6 +332,8 @@ public function testDeleteAddressById() } /** + * Test delete address from customer with incorrect address id. + * * @magentoDataFixture Magento/Customer/_files/customer.php */ public function testDeleteAddressFromCustomerBadAddressId() @@ -309,10 +347,13 @@ public function testDeleteAddressFromCustomerBadAddressId() } /** + * Test for searching addressed. + * * @param \Magento\Framework\Api\Filter[] $filters * @param \Magento\Framework\Api\Filter[] $filterGroup * @param \Magento\Framework\Api\SortOrder[] $filterOrders * @param array $expectedResult array of expected results indexed by ID + * @param int $currentPage current page for search criteria * * @dataProvider searchAddressDataProvider * @@ -320,7 +361,7 @@ public function testDeleteAddressFromCustomerBadAddressId() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoAppIsolation enabled */ - public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expectedResult) + public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expectedResult, $currentPage) { /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ $searchBuilder = $this->objectManager->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); @@ -337,7 +378,7 @@ public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expe } $searchBuilder->setPageSize(1); - $searchBuilder->setCurrentPage(2); + $searchBuilder->setCurrentPage($currentPage); $searchCriteria = $searchBuilder->create(); $searchResults = $this->repository->getList($searchCriteria); @@ -355,6 +396,11 @@ public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expe $this->assertEquals($expectedResult[$expectedResultIndex]['firstname'], $items[0]->getFirstname()); } + /** + * Data provider for searchAddresses. + * + * @return array + */ public function searchAddressDataProvider() { /** @@ -375,6 +421,7 @@ public function searchAddressDataProvider() [ ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 1 ], 'Address with city CityM' => [ [$filterBuilder->setField('city')->setValue('CityM')->create()], @@ -383,6 +430,7 @@ public function searchAddressDataProvider() [ ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 1 ], 'Addresses with firstname John sorted by firstname desc, city asc' => [ [$filterBuilder->setField('firstname')->setValue('John')->create()], @@ -395,6 +443,7 @@ public function searchAddressDataProvider() ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ['id' => 2, 'city' => 'CityX', 'postcode' => 47676, 'firstname' => 'John'], ], + 2 ], 'Addresses with postcode of either 75477 or 47676 sorted by city desc' => [ [], @@ -409,6 +458,7 @@ public function searchAddressDataProvider() ['id' => 2, 'city' => 'CityX', 'postcode' => 47676, 'firstname' => 'John'], ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 2 ], 'Addresses with postcode greater than 0 sorted by firstname asc, postcode desc' => [ [$filterBuilder->setField('postcode')->setValue('0')->setConditionType('gt')->create()], @@ -421,11 +471,14 @@ public function searchAddressDataProvider() ['id' => 2, 'city' => 'CityX', 'postcode' => 47676, 'firstname' => 'John'], ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 2 ], ]; } /** + * Test for save addresses with restricted countries. + * * @magentoDataFixture Magento/Customer/Fixtures/customer_sec_website.php */ public function testSaveAddressWithRestrictedCountries() diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php new file mode 100644 index 0000000000000..7d4e451db514b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$mutableScopeConfig = $objectManager->create(MutableScopeConfigInterface::class); + +$mutableScopeConfig->setValue( + 'customer/create_account/confirm', + 0, + ScopeInterface::SCOPE_WEBSITES, + null +); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php new file mode 100644 index 0000000000000..36743b4a20e9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Config $config */ +$config = Bootstrap::getObjectManager()->create(Config::class); +$config->deleteConfig('customer/create_account/confirm'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php new file mode 100644 index 0000000000000..c8deb7ec2a536 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$mutableScopeConfig = $objectManager->create(MutableScopeConfigInterface::class); + +$mutableScopeConfig->setValue( + 'customer/create_account/confirm', + 1, + ScopeInterface::SCOPE_WEBSITES, + null +); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php new file mode 100644 index 0000000000000..36743b4a20e9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Config $config */ +$config = Bootstrap::getObjectManager()->create(Config::class); +$config->deleteConfig('customer/create_account/confirm'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php new file mode 100644 index 0000000000000..c4f046bac57a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; +use Magento\TestFramework\Helper\Bootstrap; + +include __DIR__ . '/customer_confirmation_config_enable.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->create(CustomerRepositoryInterface::class); +/** @var CustomerInterface $customerInterface */ +$customerInterface = $objectManager->create(CustomerInterface::class); + +$customerInterface->setWebsiteId(1) + ->setEmail('customer+confirmation@example.com') + ->setConfirmation($customer->getRandomConfirmationKey()) + ->setGroupId(1) + ->setStoreId(1) + ->setFirstname('John') + ->setLastname('Smith') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customerRepository->save($customerInterface, 'password'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php new file mode 100644 index 0000000000000..7a0ebf74ed8a0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +include __DIR__ . '/customer_confirmation_config_enable_rollback.php'; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->create(CustomerRepositoryInterface::class); + +try { + $customer = $customerRepository->get('customer+confirmation@example.com'); + $customerRepository->delete($customer); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // Customer with the specified email does not exist +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php index e12eec293f2ad..1af6489870559 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php @@ -19,6 +19,7 @@ 'lastname' => 'test lastname', 'email' => 'customer@example.com', 'default_billing' => 1, + 'default_shipping' => 1, 'password' => '123123q', 'attribute_set_id' => 1, ]; diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php new file mode 100644 index 0000000000000..60b570b9d13d1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Customer\Model\CustomerRegistry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\Address; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Model\AddressRegistry; + +$objectManager = Bootstrap::getObjectManager(); +//Creating customer +/** @var $repository CustomerRepositoryInterface */ +$repository = $objectManager->create(CustomerRepositoryInterface::class); +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +$customer->setWebsiteId(1) + ->setEmail('customer_with_addresses@test.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); + +$customer->isObjectNew(true); +$customer->save(); +$customerRegistry->remove($customer->getId()); + +//Creating address +/** @var Address $customerAddress */ +$customerAddress = $objectManager->create(Address::class); +$customerAddress->isObjectNew(true); +$customerAddress->setData( + [ + 'attribute_set_id' => 2, + 'telephone' => 3468676, + 'postcode' => 75477, + 'country_id' => 'US', + 'city' => 'CityM', + 'company' => 'CompanyName', + 'street' => 'CustomerAddress1', + 'lastname' => 'Smith', + 'firstname' => 'John', + 'parent_id' => $customer->getId(), + 'region_id' => 1, + ] +); +$customerAddress->save(); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$customerAddress = $addressRepository->getById($customerAddress->getId()); +$customerAddress->setCustomerId($customer->getId()); +$customerAddress->isDefaultBilling(true); +$customerAddress->setIsDefaultShipping(true); +$customerAddress = $addressRepository->save($customerAddress); +$customerRegistry->remove($customerAddress->getCustomerId()); +/** @var AddressRegistry $addressRegistry */ +$addressRegistry = $objectManager->get(AddressRegistry::class); +$addressRegistry->remove($customerAddress->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php new file mode 100644 index 0000000000000..e0c62bffc70d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var CustomerRepositoryInterface $customerRepo */ +$customerRepo = $objectManager->get(CustomerRepositoryInterface::class); +try { + $customer = $customerRepo->get('customer_with_addresses@test.com'); + /** @var AddressRepositoryInterface $addressRepo */ + $addressRepo = $objectManager->get(AddressRepositoryInterface::class); + foreach ($customer->getAddresses() as $address) { + $addressRepo->delete($address); + } + $customerRepo->delete($customer); +} catch (NoSuchEntityException $exception) { + //Already deleted +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/three_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/three_customers_rollback.php new file mode 100644 index 0000000000000..8e13c1c25162b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/three_customers_rollback.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Integration\Model\Oauth\Token\RequestThrottler; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$customersToRemove = [ + 'customer@search.example.com', + 'customer2@search.example.com', + 'customer3@search.example.com', +]; + +/** + * @var Magento\Customer\Api\CustomerRepositoryInterface + */ +$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +/** + * @var RequestThrottler $throttler + */ +$throttler = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(RequestThrottler::class); +foreach ($customersToRemove as $customerEmail) { + try { + $customer = $customerRepository->get($customerEmail); + $customerRepository->delete($customer); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ + continue; + } + + /* Unlock account if it was locked for tokens retrieval */ + $throttler->resetAuthenticationFailuresCount($customerEmail, RequestThrottler::USER_TYPE_CUSTOMER); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php b/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php index 3bef48d8801f7..f7a47017f8b18 100644 --- a/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php +++ b/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php @@ -5,34 +5,20 @@ */ namespace Magento\Developer\Model\Logger\Handler; -use Magento\Config\Console\Command\ConfigSetCommand; -use Magento\Framework\App\Config; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Config\Setup\ConfigOptionsList; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Logger\Monolog; +use Magento\Framework\Shell; +use Magento\Setup\Mvc\Bootstrap\InitParamListener; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Deploy\Model\Mode; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use Magento\TestFramework\ObjectManager; /** - * Preconditions - * - Developer mode enabled - * - Log file isn't exists - * - 'Log to file' setting are enabled - * - * Test steps - * - Enable production mode without compilation - * - Try to log message into log file - * - Assert that log file isn't exists - * - Assert that 'Log to file' setting are disabled - * - * - Enable 'Log to file' setting - * - Try to log message into debug file - * - Assert that log file is exists - * - Assert that log file contain logged message + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DebugTest extends \PHPUnit\Framework\TestCase { @@ -42,127 +28,212 @@ class DebugTest extends \PHPUnit\Framework\TestCase private $logger; /** - * @var Mode + * @var WriteInterface */ - private $mode; + private $etcDirectory; /** - * @var InputInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ObjectManager */ - private $inputMock; + private $objectManager; /** - * @var OutputInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Shell */ - private $outputMock; + private $shell; /** - * @var ConfigSetCommand + * @var DeploymentConfig */ - private $configSetCommand; + private $deploymentConfig; /** - * @var WriteInterface + * @var string */ - private $etcDirectory; + private $debugLogPath = ''; + + /** + * @var string + */ + private static $backupFile = 'env.base.php'; + + /** + * @var string + */ + private static $configFile = 'env.php'; /** - * @var Config + * @var Debug */ - private $appConfig; + private $debugHandler; + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Exception + */ public function setUp() { + $this->objectManager = Bootstrap::getObjectManager(); + $this->shell = $this->objectManager->get(Shell::class); + $this->logger = $this->objectManager->get(Monolog::class); + $this->deploymentConfig = $this->objectManager->get(DeploymentConfig::class); + /** @var Filesystem $filesystem */ - $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); + $filesystem = $this->objectManager->create(Filesystem::class); $this->etcDirectory = $filesystem->getDirectoryWrite(DirectoryList::CONFIG); - $this->etcDirectory->copyFile('env.php', 'env.base.php'); - - $this->inputMock = $this->getMockBuilder(InputInterface::class) - ->getMockForAbstractClass(); - $this->outputMock = $this->getMockBuilder(OutputInterface::class) - ->getMockForAbstractClass(); - $this->logger = Bootstrap::getObjectManager()->get(Monolog::class); - $this->mode = Bootstrap::getObjectManager()->create( - Mode::class, - [ - 'input' => $this->inputMock, - 'output' => $this->outputMock - ] - ); - $this->configSetCommand = Bootstrap::getObjectManager()->create(ConfigSetCommand::class); - $this->appConfig = Bootstrap::getObjectManager()->create(Config::class); - - // Preconditions - $this->mode->enableDeveloperMode(); - $this->enableDebugging(); - if (file_exists($this->getDebuggerLogPath())) { - unlink($this->getDebuggerLogPath()); - } + $this->etcDirectory->copyFile(self::$configFile, self::$backupFile); } + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\FileSystemException + */ public function tearDown() { - $this->etcDirectory->delete('env.php'); - $this->etcDirectory->renameFile('env.base.php', 'env.php'); + $this->reinitDeploymentConfig(); + $this->etcDirectory->delete(self::$backupFile); } - private function enableDebugging() + /** + * @param bool $flag + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function enableDebugging(bool $flag) { - $this->inputMock = $this->getMockBuilder(InputInterface::class) - ->getMockForAbstractClass(); - $this->outputMock = $this->getMockBuilder(OutputInterface::class) - ->getMockForAbstractClass(); - $this->inputMock->expects($this->exactly(4)) - ->method('getOption') - ->withConsecutive( - [ConfigSetCommand::OPTION_LOCK_ENV], - [ConfigSetCommand::OPTION_LOCK_CONFIG], - [ConfigSetCommand::OPTION_SCOPE], - [ConfigSetCommand::OPTION_SCOPE_CODE] - ) - ->willReturnOnConsecutiveCalls( - true, - false, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - null - ); - $this->inputMock->expects($this->exactly(2)) - ->method('getArgument') - ->withConsecutive([ConfigSetCommand::ARG_PATH], [ConfigSetCommand::ARG_VALUE]) - ->willReturnOnConsecutiveCalls('dev/debug/debug_logging', 1); - $this->outputMock->expects($this->once()) - ->method('writeln') - ->with('<info>Value was saved in app/etc/env.php and locked.</info>'); - $this->assertFalse((bool)$this->configSetCommand->run($this->inputMock, $this->outputMock)); + $this->shell->execute( + PHP_BINARY . ' -f %s setup:config:set -n --%s=%s --%s=%s', + [ + BP . '/bin/magento', + ConfigOptionsList::INPUT_KEY_DEBUG_LOGGING, + (int)$flag, + InitParamListener::BOOTSTRAP_PARAM, + urldecode(http_build_query(Bootstrap::getInstance()->getAppInitParams())), + ] + ); + $this->deploymentConfig->resetData(); + $this->assertSame((int)$flag, $this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING)); } + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testDebugInProductionMode() { $message = 'test message'; + $this->reinitDebugHandler(State::MODE_PRODUCTION); - $this->mode->enableProductionModeMinimal(); + $this->removeDebugLog(); $this->logger->debug($message); $this->assertFileNotExists($this->getDebuggerLogPath()); - $this->assertFalse((bool)$this->appConfig->getValue('dev/debug/debug_logging')); + $this->assertNull($this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING)); - $this->enableDebugging(); - $this->logger->debug($message); + $this->checkCommonFlow($message); + $this->reinitDeploymentConfig(); + } + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testDebugInDeveloperMode() + { + $message = 'test message'; + $this->reinitDebugHandler(State::MODE_DEVELOPER); + $this->removeDebugLog(); + $this->logger->debug($message); $this->assertFileExists($this->getDebuggerLogPath()); $this->assertContains($message, file_get_contents($this->getDebuggerLogPath())); + $this->assertNull($this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING)); + + $this->checkCommonFlow($message); + $this->reinitDeploymentConfig(); } /** - * @return bool|string + * @return string */ private function getDebuggerLogPath() { - foreach ($this->logger->getHandlers() as $handler) { - if ($handler instanceof Debug) { - return $handler->getUrl(); + if (!$this->debugLogPath) { + foreach ($this->logger->getHandlers() as $handler) { + if ($handler instanceof Debug) { + $this->debugLogPath = $handler->getUrl(); + } } } - return false; + + return $this->debugLogPath; + } + + /** + * @throws \Magento\Framework\Exception\FileSystemException + */ + private function reinitDeploymentConfig() + { + $this->etcDirectory->delete(self::$configFile); + $this->etcDirectory->copyFile(self::$backupFile, self::$configFile); + } + + /** + * @param string $instanceMode + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function reinitDebugHandler(string $instanceMode) + { + $this->debugHandler = $this->objectManager->create( + Debug::class, + [ + 'filePath' => Bootstrap::getInstance()->getAppTempDir(), + 'state' => $this->objectManager->create( + State::class, + [ + 'mode' => $instanceMode, + ] + ), + ] + ); + $this->logger->setHandlers( + [ + $this->debugHandler, + ] + ); + } + + /** + * @return void + */ + private function detachLogger() + { + $this->debugHandler->close(); + } + + /** + * @return void + */ + private function removeDebugLog() + { + $this->detachLogger(); + if (file_exists($this->getDebuggerLogPath())) { + unlink($this->getDebuggerLogPath()); + } + } + + /** + * @param string $message + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function checkCommonFlow(string $message) + { + $this->enableDebugging(true); + $this->removeDebugLog(); + $this->logger->debug($message); + $this->assertFileExists($this->getDebuggerLogPath()); + $this->assertContains($message, file_get_contents($this->getDebuggerLogPath())); + + $this->enableDebugging(false); + $this->removeDebugLog(); + $this->logger->debug($message); + $this->assertFileNotExists($this->getDebuggerLogPath()); } } diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php new file mode 100644 index 0000000000000..8874d880a4dd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -0,0 +1,241 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Dhl\Model; + +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\Simplexml\Element; +use Magento\Shipping\Model\Tracking\Result\Error; +use Magento\Shipping\Model\Tracking\Result\Status; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class CarrierTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Dhl\Model\Carrier + */ + private $dhlCarrier; + + /** + * @var ZendClient|MockObject + */ + private $httpClientMock; + + /** + * @var \Zend_Http_Response|MockObject + */ + private $httpResponseMock; + + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->dhlCarrier = $objectManager->create( + \Magento\Dhl\Model\Carrier::class, + ['httpClientFactory' => $this->getHttpClientFactory()] + ); + } + + /** + * @magentoConfigFixture default_store carriers/dhl/id CustomerSiteID + * @magentoConfigFixture default_store carriers/dhl/password CustomerPassword + * @param string[] $trackingNumbers + * @param string $responseXml + * @param $expectedTrackingData + * @param string $expectedRequestXml + * @dataProvider getTrackingDataProvider + */ + public function testGetTracking( + $trackingNumbers, + string $responseXml, + $expectedTrackingData, + string $expectedRequestXml = '' + ) { + $this->httpResponseMock->method('getBody') + ->willReturn($responseXml); + $trackingResult = $this->dhlCarrier->getTracking($trackingNumbers); + $this->assertTrackingResult($expectedTrackingData, $trackingResult->getAllTrackings()); + if ($expectedRequestXml !== '') { + $method = new \ReflectionMethod($this->httpClientMock, '_prepareBody'); + $method->setAccessible(true); + $requestXml = $method->invoke($this->httpClientMock); + $this->assertRequest($expectedRequestXml, $requestXml); + } + } + + /** + * Get tracking data provider + * @return array + */ + public function getTrackingDataProvider() : array + { + $expectedMultiAWBRequestXml = file_get_contents(__DIR__ . '/../_files/TrackingRequest_MultipleAWB.xml'); + $multiAWBResponseXml = file_get_contents(__DIR__ . '/../_files/TrackingResponse_MultipleAWB.xml'); + $expectedSingleAWBRequestXml = file_get_contents(__DIR__ . '/../_files/TrackingRequest_SingleAWB.xml'); + $singleAWBResponseXml = file_get_contents(__DIR__ . '/../_files/TrackingResponse_SingleAWB.xml'); + $singleNoDataResponseXml = file_get_contents(__DIR__ . '/../_files/SingleknownTrackResponse-no-data-found.xml'); + $failedResponseXml = file_get_contents(__DIR__ . '/../_files/Track-res-XML-Parse-Err.xml'); + $expectedTrackingDataA = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781584780, + 'service' => 'DOCUMENT', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-25', + 'deliverytime' => '14:38:00', + 'deliverylocation' => 'BEIJING-CHN [PEK]' + ] + ], + 'weight' => '0.5 K', + ]; + $expectedTrackingDataB = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781585060, + 'service' => 'NOT RESTRICTED FOR TRANSPORT,', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-24', + 'deliverytime' => '13:35:00', + 'deliverylocation' => 'HONG KONG-HKG [HKG]' + ] + ], + 'weight' => '2.0 K', + ]; + $expectedTrackingDataC = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 5702254250, + 'service' => 'CD', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-24', + 'deliverytime' => '04:12:00', + 'deliverylocation' => 'BIRMINGHAM-GBR [BHX]' + ] + ], + 'weight' => '0.12 K', + ]; + $expectedTrackingDataD = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781585060, + 'error_message' => __('Unable to retrieve tracking') + ]; + $expectedTrackingDataE = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 111, + 'error_message' => __( + 'Error #%1 : %2', + '111', + ' Error Parsing incoming request XML + Error: The content of element type + "ShipperReference" must match + "(ReferenceID,ReferenceType?)". at line + 16, column 22' + ) + ]; + return [ + 'multi-AWB' => [ + ['4781584780', '4781585060', '5702254250'], + $multiAWBResponseXml, + [$expectedTrackingDataA, $expectedTrackingDataB, $expectedTrackingDataC], + $expectedMultiAWBRequestXml + ], + 'single-AWB' => [ + ['4781585060'], + $singleAWBResponseXml, + [$expectedTrackingDataB], + $expectedSingleAWBRequestXml + ], + 'single-AWB-no-data' => [ + ['4781585061'], + $singleNoDataResponseXml, + [$expectedTrackingDataD] + ], + 'failed-response' => [ + ['4781585060-failed'], + $failedResponseXml, + [$expectedTrackingDataE] + ] + ]; + } + + /** + * Get mocked Http Client Factory + * + * @return MockObject + */ + private function getHttpClientFactory(): MockObject + { + $this->httpResponseMock = $this->getMockBuilder(\Zend_Http_Response::class) + ->disableOriginalConstructor() + ->getMock(); + $this->httpClientMock = $this->getMockBuilder(ZendClient::class) + ->disableOriginalConstructor() + ->setMethods(['request']) + ->getMock(); + $this->httpClientMock->method('request') + ->willReturn($this->httpResponseMock); + /** @var ZendClientFactory|MockObject $httpClientFactoryMock */ + $httpClientFactoryMock = $this->getMockBuilder(ZendClientFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $httpClientFactoryMock->method('create') + ->willReturn($this->httpClientMock); + + return $httpClientFactoryMock; + } + + /** + * Assert request + * + * @param string $expectedRequestXml + * @param string $requestXml + */ + private function assertRequest(string $expectedRequestXml, string $requestXml): void + { + $expectedRequestElement = new Element($expectedRequestXml); + $requestElement = new Element($requestXml); + $requestMessageTime = $requestElement->Request->ServiceHeader->MessageTime->__toString(); + $this->assertEquals( + 1, + preg_match("/\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}\+\d{2}\:\d{2}/", $requestMessageTime) + ); + $expectedRequestElement->Request->ServiceHeader->MessageTime = $requestMessageTime; + $messageReference = $requestElement->Request->ServiceHeader->MessageReference->__toString(); + $this->assertStringStartsWith('MAGE_TRCK_', $messageReference); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_TRCK_28TO32_Char_CHECKED'; + $this->assertXmlStringEqualsXmlString($expectedRequestElement->asXML(), $requestElement->asXML()); + } + + /** + * Assert tracking + * + * @param array|null $expectedTrackingData + * @param Status[]|null $trackingResults + * @return void + */ + private function assertTrackingResult($expectedTrackingData, $trackingResults): void + { + if (null === $expectedTrackingData) { + $this->assertNull($trackingResults); + } else { + $ctr = 0; + foreach ($trackingResults as $trackingResult) { + $this->assertEquals($expectedTrackingData[$ctr++], $trackingResult->getData()); + } + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml new file mode 100755 index 0000000000000..9887cecbd2d4e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:59:34+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>No Shipments Found</ActionStatus> + <Condition> + <ConditionCode>209</ConditionCode> + <ConditionData>No Shipments Found for AWBNumber 6017300993</ConditionData> + </Condition> + </Status> + </AWBInfo> + <LanguageCode>String</LanguageCode> +</req:TrackingResponse> +<!-- ServiceInvocationId:20180227125934_5793_74fbd9e1-a8b0-4f6a-a326-26aae979e5f0 --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml new file mode 100755 index 0000000000000..c2abd68d3c4ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentTrackingErrorResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com track-err-res.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:55:05+01:00</MessageTime> + </ServiceHeader> + <Status> + <ActionStatus>Failure</ActionStatus> + <Condition> + <ConditionCode>111</ConditionCode> + <ConditionData> Error Parsing incoming request XML + Error: The content of element type + "ShipperReference" must match + "(ReferenceID,ReferenceType?)". at line + 16, column 22</ConditionData> + </Condition> + </Status> + </Response> +</req:ShipmentTrackingErrorResponse> +<!-- ServiceInvocationId:20180227125505_5793_2008671c-9292-4790-87b6-b02ccdf913db --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml new file mode 100755 index 0000000000000..c0a18fcc4e2f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:KnownTrackingRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd" schemaVersion="1.0"> + <Request> + <ServiceHeader> + <MessageTime>2002-06-25T11:28:56-08:00</MessageTime> + <MessageReference>MAGE_TRCK_28TO32_Char_CHECKED</MessageReference> + <SiteID>CustomerSiteID</SiteID> + <Password>CustomerPassword</Password> + </ServiceHeader> + </Request> + <LanguageCode>en</LanguageCode> + <AWBNumber>4781584780</AWBNumber> + <AWBNumber>4781585060</AWBNumber> + <AWBNumber>5702254250</AWBNumber> + <LevelOfDetails>ALL_CHECK_POINTS</LevelOfDetails> +</req:KnownTrackingRequest> + + diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml new file mode 100755 index 0000000000000..dac69a0d68c57 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:KnownTrackingRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd" schemaVersion="1.0"> + <Request> + <ServiceHeader> + <MessageTime>2002-06-25T11:28:56-08:00</MessageTime> + <MessageReference>MAGE_TRCK_28TO32_Char_CHECKED</MessageReference> + <SiteID>CustomerSiteID</SiteID> + <Password>CustomerPassword</Password> + </ServiceHeader> + </Request> + <LanguageCode>en</LanguageCode> + <AWBNumber>4781585060</AWBNumber> + <LevelOfDetails>ALL_CHECK_POINTS</LevelOfDetails> +</req:KnownTrackingRequest> \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml new file mode 100755 index 0000000000000..369236d80c614 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:43:44+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781584780</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>PEK</ServiceAreaCode> + <Description>BEIJING-CHN</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>PHL</ServiceAreaCode> + <Description>WEST PHILADELPHIA,PA-USA</Description> + </DestinationServiceArea> + <ShipperName>THE EXP HIGH SCH ATT TO BNU</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>HAVEFORD COLLEGE</ConsigneeName> + <ShipmentDate>2017-12-25T14:38:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>0.5</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>D</GlobalProductCode> + <ShipmentDesc>DOCUMENT</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>BEIJING</City> + <PostalCode>100032</PostalCode> + <CountryCode>CN</CountryCode> + </Shipper> + <Consignee> + <City>HAVERFORD</City> + <DivisionCode>PA</DivisionCode> + <PostalCode>19041</PostalCode> + <CountryCode>US</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>2469</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-25</Date> + <Time>14:38:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>PEK</ServiceAreaCode> + <Description>BEIJING-CHN</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </DestinationServiceArea> + <ShipperName>NET-A-PORTER</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>NICOLE LI</ConsigneeName> + <ShipmentDate>2017-12-24T13:35:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>2.0</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>N</GlobalProductCode> + <ShipmentDesc>NOT RESTRICTED FOR TRANSPORT,</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>HONG KONG</City> + <CountryCode>HK</CountryCode> + </Shipper> + <Consignee> + <City>HONG KONG</City> + <DivisionCode>CH</DivisionCode> + <CountryCode>HK</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>1060571</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>13:35:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <AWBInfo> + <AWBNumber>5702254250</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>BHX</ServiceAreaCode> + <Description>BIRMINGHAM-GBR</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>AOI</ServiceAreaCode> + <Description>ANCONA-ITA</Description> + </DestinationServiceArea> + <ShipperName>AMAZON EU SARL</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>MATTEO LOMBO</ConsigneeName> + <ShipmentDate>2017-12-24T04:12:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>0.12</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>U</GlobalProductCode> + <ShipmentDesc>CD</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>PETERBOROUGH</City> + <PostalCode>PE2 9EN</PostalCode> + <CountryCode>GB</CountryCode> + </Shipper> + <Consignee> + <City>ORTONA</City> + <PostalCode>66026</PostalCode> + <CountryCode>IT</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>DGWYDy4xN_1</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>04:12:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>BHX</ServiceAreaCode> + <Description>BIRMINGHAM-GBR</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <LanguageCode>en</LanguageCode> +</req:TrackingResponse> +<!-- ServiceInvocationId:20180227124344_5793_23bed3d9-e792-4955-8055-9472b1b41929 --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml new file mode 100755 index 0000000000000..ef303eaab64f7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:27:42+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </DestinationServiceArea> + <ShipperName>NET-A-PORTER</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>NICOLE LI</ConsigneeName> + <ShipmentDate>2017-12-24T13:35:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>2.0</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>N</GlobalProductCode> + <ShipmentDesc>NOT RESTRICTED FOR TRANSPORT,</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>HONG KONG</City> + <CountryCode>HK</CountryCode> + </Shipper> + <Consignee> + <City>HONG KONG</City> + <DivisionCode>CH</DivisionCode> + <CountryCode>HK</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>1060571</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>13:35:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <LanguageCode>en</LanguageCode> +</req:TrackingResponse> +<!-- ServiceInvocationId:20180227122741_5793_e0f8c40e-5245-4737-ab31-323030366721 --> diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php index b620d9097b4be..10f2749ddace1 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php @@ -56,7 +56,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @magentoDataFixture Magento/Store/_files/store.php @@ -77,7 +77,7 @@ public function testGetConfigCurrencies(string $areaCode, array $expected) $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $storeManager->setCurrentStore($store->getId()); - if ($areaCode === Area::AREA_ADMINHTML) { + if (in_array($areaCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { self::assertEquals($expected['allowed'], $this->currency->getConfigAllowCurrencies()); self::assertEquals($expected['base'], $this->currency->getConfigBaseCurrencies()); self::assertEquals($expected['default'], $this->currency->getConfigDefaultCurrencies()); @@ -118,6 +118,14 @@ public function getConfigCurrenciesDataProvider() 'default' => ['BDT', 'USD'], ], ], + [ + 'areaCode' => Area::AREA_CRONTAB, + 'expected' => [ + 'allowed' => ['BDT', 'BNS', 'BTD', 'EUR', 'USD'], + 'base' => ['BDT', 'USD'], + 'default' => ['BDT', 'USD'], + ], + ], [ 'areaCode' => Area::AREA_FRONTEND, 'expected' => [ diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php index c743fcec1dd89..b07a6506c1b78 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class LinksTest + * + * @package Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links + */ class LinksTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php index 28d3680358329..3f3b3bd621953 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class SamplesTest + * + * @package Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples + */ class SamplesTest extends \PHPUnit\Framework\TestCase { public function testGetUploadButtonsHtml() diff --git a/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php b/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php index c80cd13a1683b..d0e4471e2ea68 100644 --- a/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php +++ b/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php @@ -9,7 +9,10 @@ class DownloadableTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ 'downloadable-product' => [ @@ -31,79 +34,32 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - - /** - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider exportImportDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @todo remove after MAGETWO-38240 resolved - */ - public function testExport($fixtures, $skus, $skippedAttributes = [], $rollbackFixtures = []) - { - $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); - } - - /** - * @param array $fixtures - * @param string[] $skus - * @dataProvider exportImportDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @todo remove after MAGETWO-38240 resolved - */ - public function testImportDelete($fixtures, $skus, $skippedAttributes = [], $rollbackFixtures = []) - { - $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); - } - /** - * @magentoAppArea adminhtml - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * Run import/export tests. * - * @todo remove after MAGETWO-38240 resolved - */ - public function testImportReplace($fixtures, $skus, $skippedAttributes = [], $rollbackFixtures = []) - { - $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); - } - - /** * @magentoAppArea adminhtml - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation enabled * * @param array $fixtures * @param string[] $skus * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - * + * @return void + * @dataProvider exportImportDataProvider * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function testImportReplaceWithPagination($fixtures, $skus, $skippedAttributes = []) + public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void { $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); } /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { $expectedProductLinks = $expectedProduct->getExtensionAttributes()->getDownloadableProductLinks(); $expectedProductSamples = $expectedProduct->getExtensionAttributes()->getDownloadableProductSamples(); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php b/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php index 5d7a72c65597d..a5843f20ad98a 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php @@ -55,7 +55,7 @@ public function addAttributeDataProvider() { return [ ['eav_setup_test'], - ['_59_characters_59_characters_59_characters_59_characters_59'], + ['characters_59_characters_59_characters_59_characters_59_59_'], ]; } @@ -90,6 +90,33 @@ public function addAttributeThrowExceptionDataProvider() ]; } + /** + * Verify that add attribute throw exception if attribute_code is not valid. + * + * @param string|null $attributeCode + * + * @dataProvider addInvalidAttributeThrowExceptionDataProvider + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, + */ + public function testAddInvalidAttributeThrowException($attributeCode) + { + $attributeData = $this->getAttributeData(); + $this->eavSetup->addAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, $attributeData); + } + /** + * Data provider for testAddInvalidAttributeThrowException(). + * + * @return array + */ + public function addInvalidAttributeThrowExceptionDataProvider() + { + return [ + ['1first_character_is_not_letter'], + ['attribute.with.dots'], + ]; + } + /** * Get simple attribute data. */ diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php index 978815f665341..a52c5bb9e21b7 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php @@ -43,7 +43,7 @@ protected function setUp() $contentManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class) ->disableOriginalConstructor() ->getMock(); - $this->clientMock = $this->getMockBuilder(\Magento\Elasticsearch\Model\Client\Elasticsearch::class) + $this->clientMock = $this->getMockBuilder(\Magento\Elasticsearch6\Model\Client\Elasticsearch::class) ->disableOriginalConstructor() ->getMock(); $contentManager @@ -78,7 +78,7 @@ protected function setUp() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php index 61add5f7d0ea7..3eea2497daa1f 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php @@ -10,7 +10,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; +use Magento\Elasticsearch6\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -95,7 +95,7 @@ private function search($text) } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductName() @@ -104,7 +104,7 @@ public function testSearchConfigurableProductBySimpleProductName() } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductAttributeMultiselect() @@ -113,7 +113,7 @@ public function testSearchConfigurableProductBySimpleProductAttributeMultiselect } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductAttributeSelect() @@ -122,7 +122,7 @@ public function testSearchConfigurableProductBySimpleProductAttributeSelect() } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductAttributeShortDescription() diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php index 014aaf7679bc9..77533e83b719c 100755 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php @@ -13,7 +13,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; +use Magento\Elasticsearch6\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; use Magento\Indexer\Model\Indexer; @@ -87,7 +87,7 @@ protected function setUp() } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @return void */ @@ -106,7 +106,7 @@ public function testReindexAll(): void /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @return void */ @@ -131,7 +131,7 @@ public function testReindexRowAfterEdit(): void } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @return void */ @@ -170,7 +170,7 @@ public function testReindexRowAfterMassAction(): void } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @magentoAppArea adminhtml * @return void @@ -192,7 +192,7 @@ public function testReindexRowAfterDelete(): void /** * @magentoDbIsolation enabled * @magentoAppArea adminhtml - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @magentoDataFixture Magento/Elasticsearch/_files/configurable_products.php * @return void diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php index d40ce9e8a0706..7d4aa8e005e4e 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php @@ -68,7 +68,7 @@ protected function setUp() /** * Test search of all products after full reindex * - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest_configurable * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php */ @@ -82,7 +82,7 @@ public function testSearchAll() /** * Test search of specific product after full reindex * - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest_configurable * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php */ diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php index dc288a18fadb7..a3da32e0d6c40 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php @@ -5,8 +5,6 @@ */ namespace Magento\Elasticsearch\SearchAdapter; -use Magento\Elasticsearch\Model\Config; - /** * Class AdapterTest * @@ -26,7 +24,7 @@ class AdapterTest extends \Magento\Framework\Search\Adapter\Mysql\AdapterTest /** * @var string */ - protected $searchEngine = Config::ENGINE_NAME; + protected $searchEngine = 'elasticsearch6'; /** * Get request config path @@ -43,12 +41,12 @@ protected function getRequestConfigPath() */ protected function createAdapter() { - return $this->objectManager->create(\Magento\Elasticsearch\SearchAdapter\Adapter::class); + return $this->objectManager->create(\Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter::class); } /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testMatchQuery() @@ -58,7 +56,7 @@ public function testMatchQuery() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testMatchOrderedQuery() @@ -70,7 +68,7 @@ public function testMatchOrderedQuery() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testAggregationsQuery() @@ -80,7 +78,7 @@ public function testAggregationsQuery() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testMatchQueryFilters() @@ -92,7 +90,7 @@ public function testMatchQueryFilters() * Range filter test with all fields filled * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testRangeFilterWithAllFields() @@ -104,7 +102,7 @@ public function testRangeFilterWithAllFields() * Range filter test with all fields filled * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testRangeFilterWithoutFromField() @@ -116,7 +114,7 @@ public function testRangeFilterWithoutFromField() * Range filter test with all fields filled * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testRangeFilterWithoutToField() @@ -128,7 +126,7 @@ public function testRangeFilterWithoutToField() * Term filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testTermFilter() @@ -140,7 +138,7 @@ public function testTermFilter() * Term filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testTermFilterArray() @@ -152,7 +150,7 @@ public function testTermFilterArray() * Term filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testWildcardFilter() @@ -164,7 +162,7 @@ public function testWildcardFilter() * Request limits test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testSearchLimit() @@ -176,7 +174,7 @@ public function testSearchLimit() * Bool filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testBoolFilter() @@ -188,7 +186,7 @@ public function testBoolFilter() * Test bool filter with nested negative bool filter * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testBoolFilterWithNestedNegativeBoolFilter() @@ -200,7 +198,7 @@ public function testBoolFilterWithNestedNegativeBoolFilter() * Test range inside nested negative bool filter * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() @@ -213,7 +211,7 @@ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() * * @dataProvider elasticSearchAdvancedSearchDataProvider * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @param string $nameQuery * @param string $descriptionQuery @@ -259,7 +257,7 @@ public function elasticSearchAdvancedSearchDataProvider() /** * @magentoAppIsolation enabled * @magentoDataFixture Magento/Framework/Search/_files/filterable_attribute.php - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testCustomFilterableAttribute() @@ -274,7 +272,7 @@ public function testCustomFilterableAttribute() * * @magentoAppIsolation enabled * @magentoDataFixture Magento/Framework/Search/_files/filterable_attributes.php - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @dataProvider filterByAttributeValuesDataProvider * @param string $requestName @@ -294,7 +292,7 @@ public function testFilterByAttributeValues($requestName, $additionalData) * @param $rangeFilter * @param $expectedRecordsCount * @magentoDataFixture Magento/Framework/Search/_files/date_attribute.php - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @magentoAppIsolation enabled * @dataProvider dateDataProvider @@ -309,7 +307,7 @@ public function testAdvancedSearchDateField($rangeFilter, $expectedRecordsCount) /** * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testAdvancedSearchCompositeProductWithOutOfStockOption() @@ -320,7 +318,7 @@ public function testAdvancedSearchCompositeProductWithOutOfStockOption() /** * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_disabled_child.php * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testAdvancedSearchCompositeProductWithDisabledChild() @@ -333,7 +331,7 @@ public function testAdvancedSearchCompositeProductWithDisabledChild() /** * @magentoDataFixture Magento/Framework/Search/_files/search_weight_products.php * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testSearchQueryBoost() @@ -369,4 +367,18 @@ public function dateDataProvider() [['from' => '2000-02-01T00:00:00Z', 'to' => ''], 0], ]; } + + public function filterByAttributeValuesDataProvider() + { + $variations = parent::filterByAttributeValuesDataProvider(); + + $variations['quick search by date'] = [ + 'quick_search_container', + [ + 'search_term' => '2000-10-30', + ], + ]; + + return $variations; + } } diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php index 22e8a5911f084..822c3c031886c 100644 --- a/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php @@ -72,7 +72,7 @@ public function testSaveActionWithInvalidKey() $this->assertRedirect(); $this->assertSessionMessages( - $this->contains('The encryption key format is invalid.'), + $this->contains('Encryption key must be 32 character string without any white space.'), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php index 3a47692bdb932..a563641a4f874 100644 --- a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php @@ -41,6 +41,9 @@ public function testChangeEncryptionKey() $structureMock->expects($this->once()) ->method('getFieldPathsByAttribute') ->will($this->returnValue([$testPath])); + $structureMock->expects($this->once()) + ->method('getFieldPaths') + ->willReturn([]); /** @var \Magento\Config\Model\ResourceModel\Config $configModel */ $configModel = $this->objectManager->create(\Magento\Config\Model\ResourceModel\Config::class); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php new file mode 100644 index 0000000000000..0e1b51b3ae273 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php @@ -0,0 +1,85 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Code\Generator; + +use Magento\Framework\Code\Generator; +use Magento\Framework\Logger\Monolog as MagentoMonologLogger; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Log\LoggerInterface; + +class AutoloaderTest extends TestCase +{ + /** + * This method exists to fix the wrong return type hint on \Magento\Framework\App\ObjectManager::getInstance. + * This way the IDE knows it's dealing with an instance of \Magento\TestFramework\ObjectManager and + * not \Magento\Framework\App\ObjectManager. The former has the method addSharedInstance, the latter does not. + * + * @return ObjectManager|\Magento\Framework\App\ObjectManager + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getTestFrameworkObjectManager() + { + return ObjectManager::getInstance(); + } + + /** + * @before + */ + public function setupLoggerTestDouble(): void + { + $loggerTestDouble = $this->createMock(LoggerInterface::class); + $this->getTestFrameworkObjectManager()->addSharedInstance($loggerTestDouble, MagentoMonologLogger::class); + } + + /** + * @after + */ + public function removeLoggerTestDouble(): void + { + $this->getTestFrameworkObjectManager()->removeSharedInstance(MagentoMonologLogger::class); + } + + /** + * @param \RuntimeException $testException + * @return Generator|MockObject + */ + private function createExceptionThrowingGeneratorTestDouble(\RuntimeException $testException) + { + /** @var Generator|MockObject $generatorStub */ + $generatorStub = $this->createMock(Generator::class); + $generatorStub->method('generateClass')->willThrowException($testException); + + return $generatorStub; + } + + public function testLogsExceptionDuringGeneration(): void + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $this->assertNull($autoloader->load(NonExistingClassName::class)); + } + + public function testFiltersDuplicateExceptionMessages(): void + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $autoloader->load(OneNonExistingClassName::class); + $autoloader->load(AnotherNonExistingClassName::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php index e42a79aa52596..01e63126e5cde 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php @@ -7,6 +7,9 @@ use Zend\Code\Generator\DocBlockGenerator; +/** + * phpcs:ignoreFile + */ class ParentClassWithNamespace { /** @@ -78,9 +81,6 @@ public static function publicParentStatic() { } - /** - * @SuppressWarnings(PHPMD.FinalImplementation) Suppressed as is a fixture but not a real code - */ final public function publicParentFinal() { } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php index e5bce7c52db7a..b9fc351ff64e6 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php @@ -8,7 +8,7 @@ use Zend\Code\Generator\ClassGenerator; /** - * Class SourceClassWithNamespace + * phpcs:ignoreFile */ class SourceClassWithNamespace extends ParentClassWithNamespace { @@ -115,8 +115,6 @@ public static function publicChildStatic() /** * Test method - * - * @SuppressWarnings(PHPMD.FinalImplementation) Suppressed as is a fixture but not a real code */ final public function publicChildFinal() { 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 cf3b9f05cbe0f..403c45dde71a3 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 @@ -8,6 +8,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\TestFramework\Helper\CacheCleaner; use Magento\Framework\DB\Ddl\Table; +use Magento\TestFramework\Helper\Bootstrap; class MysqlTest extends \PHPUnit\Framework\TestCase { @@ -19,7 +20,7 @@ class MysqlTest extends \PHPUnit\Framework\TestCase protected function setUp() { set_error_handler(null); - $this->resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $this->resourceConnection = Bootstrap::getObjectManager() ->get(ResourceConnection::class); CacheCleaner::cleanAll(); } @@ -40,7 +41,6 @@ public function testWaitTimeout() $this->markTestSkipped('This test is for \Magento\Framework\DB\Adapter\Pdo\Mysql'); } try { - $defaultWaitTimeout = $this->getWaitTimeout(); $minWaitTimeout = 1; $this->setWaitTimeout($minWaitTimeout); $this->assertEquals($minWaitTimeout, $this->getWaitTimeout(), 'Wait timeout was not changed'); @@ -49,17 +49,8 @@ public function testWaitTimeout() sleep($minWaitTimeout + 1); $result = $this->executeQuery('SELECT 1'); $this->assertInstanceOf(\Magento\Framework\DB\Statement\Pdo\Mysql::class, $result); - // Restore wait_timeout - $this->setWaitTimeout($defaultWaitTimeout); - $this->assertEquals( - $defaultWaitTimeout, - $this->getWaitTimeout(), - 'Default wait timeout was not restored' - ); - } catch (\Exception $e) { - // Reset connection on failure to restore global variables + } finally { $this->getDbAdapter()->closeConnection(); - throw $e; } } @@ -87,30 +78,14 @@ private function setWaitTimeout($waitTimeout) /** * Execute SQL query and return result statement instance * - * @param string $sql - * @return \Zend_Db_Statement_Interface - * @throws \Exception + * @param $sql + * @return void|\Zend_Db_Statement_Pdo + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Adapter_Exception */ private function executeQuery($sql) { - /** - * Suppress PDO warnings to work around the bug https://bugs.php.net/bug.php?id=63812 - */ - $phpErrorReporting = error_reporting(); - /** @var $pdoConnection \PDO */ - $pdoConnection = $this->getDbAdapter()->getConnection(); - $pdoWarningsEnabled = $pdoConnection->getAttribute(\PDO::ATTR_ERRMODE) & \PDO::ERRMODE_WARNING; - if (!$pdoWarningsEnabled) { - error_reporting($phpErrorReporting & ~E_WARNING); - } - try { - $result = $this->getDbAdapter()->query($sql); - error_reporting($phpErrorReporting); - } catch (\Exception $e) { - error_reporting($phpErrorReporting); - throw $e; - } - return $result; + return $this->getDbAdapter()->query($sql); } /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php b/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php index a934372bfd907..9980f40239f8c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php @@ -4,41 +4,51 @@ * See COPYING.txt for license details. */ +namespace Magento\Framework\Data\Form\Element; + +use Magento\Framework\Data\Form\ElementFactory; +use Magento\TestFramework\Helper\Bootstrap; + /** * Tests for \Magento\Framework\Data\Form\Element\Date */ -namespace Magento\Framework\Data\Form\Element; - class DateTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\Data\Form\ElementFactory + * @var ElementFactory */ - protected $_elementFactory; + private $elementFactory; /** - * SetUp method + * @inheritdoc */ protected function setUp() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_elementFactory = $objectManager->create(\Magento\Framework\Data\Form\ElementFactory::class); + $objectManager = Bootstrap::getObjectManager(); + $this->elementFactory = $objectManager->create(ElementFactory::class); } /** + * Test get value + * + * @param array $data + * @param string $expect + * @return void * @dataProvider getValueDataProvider */ - public function testGetValue(array $data, $expect) + public function testGetValue(array $data, string $expect): void { - /** @var $date \Magento\Framework\Data\Form\Element\Date */ - $date = $this->_elementFactory->create(\Magento\Framework\Data\Form\Element\Date::class, $data); + /** @var $date Date */ + $date = $this->elementFactory->create(Date::class, $data); $this->assertEquals($expect, $date->getValue()); } /** + * Get value test data provider + * * @return array */ - public function getValueDataProvider() + public function getValueDataProvider(): array { $testTimestamp = strtotime('2014-05-18 12:08:16'); $date = new \DateTime('@' . $testTimestamp); @@ -56,15 +66,22 @@ public function getValueDataProvider() 'time_format' => 'h:mm a', 'value' => $testTimestamp, ], - $date->format('g:i A') + $date->format('g:i A'), ], [ [ 'date_format' => 'MM/d/yy', 'value' => $testTimestamp, ], - $date->format('m/j/y') - ] + $date->format('m/j/y'), + ], + [ + [ + 'date_format' => 'd-MM-Y', + 'value' => $date->format('d-m-Y'), + ], + $date->format('d-m-Y'), + ], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php index 2ba9109df86ed..88c567da75292 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php @@ -10,18 +10,64 @@ class EncryptorTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Framework\Encryption\Encryptor */ - protected $_model; + private $encryptor; protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->encryptor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Framework\Encryption\Encryptor::class ); } public function testEncryptDecrypt() { - $this->assertEquals('', $this->_model->decrypt($this->_model->encrypt(''))); - $this->assertEquals('test', $this->_model->decrypt($this->_model->encrypt('test'))); + $this->assertEquals('', $this->encryptor->decrypt($this->encryptor->encrypt(''))); + $this->assertEquals('test', $this->encryptor->decrypt($this->encryptor->encrypt('test'))); + } + + /** + * @param string $key + * @dataProvider validEncryptionKeyDataProvider + */ + public function testValidateKey($key) + { + $this->encryptor->validateKey($key); + } + + public function validEncryptionKeyDataProvider() + { + return [ + '32 numbers' => ['12345678901234567890123456789012'], + '32 characters' => ['aBcdeFghIJKLMNOPQRSTUvwxYzabcdef'], + '32 special characters' => ['!@#$%^&*()_+~`:;"<>,.?/|*&^%$#@!'], + '32 combination' =>['1234eFghI1234567^&*(890123456789'], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Encryption key must be 32 character string without any white space. + * + * @param string $key + * @dataProvider invalidEncryptionKeyDataProvider + */ + public function testValidateKeyInvalid($key) + { + $this->encryptor->validateKey($key); + } + + public function invalidEncryptionKeyDataProvider() + { + return [ + 'empty string' => [''], + 'leading space' => [' 1234567890123456789012345678901'], + 'tailing space' => ['1234567890123456789012345678901 '], + 'space in the middle' => ['12345678901 23456789012345678901'], + 'tab in the middle' => ['12345678901 23456789012345678'], + 'return in the middle' => ['12345678901 + 23456789012345678901'], + '31 characters' => ['1234567890123456789012345678901'], + '33 characters' => ['123456789012345678901234567890123'], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index 7f8996daa6e97..10a6b9d8caae4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -8,6 +8,7 @@ namespace Magento\Framework\GraphQl\Config; use Magento\Framework\App\Cache; +use Magento\Framework\App\Request\Http; use Magento\Framework\GraphQl\Config; use Magento\Framework\GraphQl\Schema\SchemaGenerator; use Magento\Framework\ObjectManagerInterface; @@ -175,8 +176,9 @@ enumValues(includeDeprecated: true) { 'operationName' => 'IntrospectionQuery' ]; /** @var Http $request */ - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); + $request = $this->objectManager->get(Http::class); $request->setPathInfo('/graphql'); + $request->setMethod('POST'); $request->setContent(json_encode($postData)); $headers = $this->objectManager->create(\Zend\Http\Headers::class) ->addHeaders(['Content-Type' => 'application/json']); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php index 0a75aba5c9c48..a35596cf0f907 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php @@ -8,7 +8,7 @@ namespace Magento\Framework\Interception\Fixture; /** - * @codingStandardsIgnoreStart + * phpcs:ignoreFile */ class Intercepted extends InterceptedParent implements InterceptedInterface { @@ -49,7 +49,6 @@ public function D($param1) /** * @SuppressWarnings(PHPMD.ShortMethodName) - * @SuppressWarnings(PHPMD.FinalImplementation) Suppressed as is a fixture but not a real code */ final public function E($param1) { diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php new file mode 100644 index 0000000000000..e64b3c505acf1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +/** + * \Magento\Framework\Lock\Backend\File test case + */ +class FileLockTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Lock\Backend\FileLock + */ + private $model; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + \Magento\Framework\Lock\Backend\FileLock::class, + ['path' => '/tmp'] + ); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php new file mode 100644 index 0000000000000..8d0caad5d55e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\App\DeploymentConfig\FileReader; +use Magento\Framework\Stdlib\ArrayManager; + +/** + * \Magento\Framework\Lock\Backend\Zookeeper test case + */ +class ZookeeperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FileReader + */ + private $configReader; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var LockBackendFactory + */ + private $lockBackendFactory; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var ZookeeperLock + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + if (!extension_loaded('zookeeper')) { + $this->markTestSkipped('php extension Zookeeper is not installed.'); + } + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->configReader = $this->objectManager->get(FileReader::class); + $this->lockBackendFactory = $this->objectManager->create(LockBackendFactory::class); + $this->arrayManager = $this->objectManager->create(ArrayManager::class); + $config = $this->configReader->load(ConfigFilePool::APP_ENV); + + if ($this->arrayManager->get('lock/provider', $config) !== 'zookeeper') { + $this->markTestSkipped('Zookeeper is not configured during installation.'); + } + + $this->model = $this->lockBackendFactory->create(); + $this->assertInstanceOf(ZookeeperLock::class, $this->model); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php index 189d189d32c97..c2521c27a0c77 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\MessageQueue; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\PreconditionFailedException; + /** * @see dev/tests/integration/_files/Magento/TestModuleMessageQueueConfiguration * @see dev/tests/integration/_files/Magento/TestModuleMessageQueueConfigOverride @@ -25,7 +28,12 @@ class TopologyTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->helper = new \Magento\TestFramework\Helper\Amqp(); + $this->helper = Bootstrap::getObjectManager()->create(\Magento\TestFramework\Helper\Amqp::class); + + if (!$this->helper->isAvailable()) { + $this->fail('This test relies on RabbitMQ Management Plugin.'); + } + $this->declaredExchanges = $this->helper->getExchanges(); } @@ -39,6 +47,7 @@ public function testTopologyInstallation(array $expectedConfig, array $bindingCo $name = $expectedConfig['name']; $this->assertArrayHasKey($name, $this->declaredExchanges); unset($this->declaredExchanges[$name]['message_stats']); + unset($this->declaredExchanges[$name]['user_who_performed_action']); $this->assertEquals( $expectedConfig, $this->declaredExchanges[$name], diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml index 0fc50f0432b93..1cc5d6cd3b714 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> <topic name="topic.broker.test" request="string" response="string"> - <handler name="topicBrokerHandler" type="Magento\MysqlMq\Model\Processor" method="processMessage"/> + <handler name="topicBrokerHandler" type="Magento\TestModuleMysqlMq\Model\Processor" method="processMessage"/> </topic> </config> diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php index b5f5145c32c72..9a813b4424eaa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php @@ -40,7 +40,7 @@ "name" => "publisher5.topic", "schema" => [ "schema_type" => "object", - "schema_value" => '\\' . \Magento\MysqlMq\Model\DataObject::class + "schema_value" => '\\' . \Magento\TestModuleMysqlMq\Model\DataObject::class ], "response_schema" => [ "schema_type" => "object", @@ -58,7 +58,7 @@ "handlers" => [ "topic.broker.test" => [ "0" => [ - "type" => \Magento\MysqlMq\Model\Processor::class, + "type" => \Magento\TestModuleMysqlMq\Model\Processor::class, "method" => "processMessage" ] ] diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php index fdd4a7d3007a7..ed6e13cfe9fae 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php @@ -23,11 +23,11 @@ "name" => "publisher5.topic", "schema" => [ "schema_type" => "object", - "schema_value" => "Magento\\MysqlMq\\Model\\DataObject" + "schema_value" => \Magento\TestModuleMysqlMq\Model\DataObject::class ], "response_schema" => [ "schema_type" => "object", - "schema_value" => "Magento\\Customer\\Api\\Data\\CustomerInterface" + "schema_value" => \Magento\Customer\Api\Data\CustomerInterface::class ], "publisher" => "test-publisher-5" ] diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php index b09af48b5f943..f4f3337a253c0 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php @@ -20,6 +20,10 @@ CategorySetup::class, ['resourceName' => 'catalog_setup'] ); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); + $selectOptions = []; $selectAttributes = []; foreach (range(1, 2) as $index) { @@ -30,7 +34,7 @@ $selectAttribute->setData( [ 'attribute_code' => 'select_attribute_' . $index, - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'entity_type_id' => $productEntityTypeId, 'is_global' => 1, 'is_user_defined' => 1, 'frontend_input' => 'select', @@ -56,7 +60,8 @@ ); $selectAttribute->save(); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $selectAttribute->getId()); + $installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $selectAttribute->getId()); + /** @var $selectOptions Collection */ $selectOption = Bootstrap::getObjectManager()->create( Collection::class @@ -65,6 +70,26 @@ $selectAttributes[$index] = $selectAttribute; $selectOptions[$index] = $selectOption; } + +$dateAttribute = Bootstrap::getObjectManager()->create(Attribute::class); +$dateAttribute->setData( + [ + 'attribute_code' => 'date_attribute', + 'entity_type_id' => $productEntityTypeId, + 'is_global' => 1, + 'is_filterable' => 1, + 'backend_type' => 'datetime', + 'frontend_input' => 'date', + 'frontend_label' => 'Test Date', + 'is_searchable' => 1, + 'is_filterable_in_search' => 1, + ] +); +$dateAttribute->save(); +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $dateAttribute->getId()); + +$productAttributeSetId = $installer->getAttributeSetId($productEntityTypeId, 'Default'); /* Create simple products per each first attribute option */ foreach ($selectOptions[1] as $option) { /** @var $product Product */ @@ -74,7 +99,7 @@ $product->setTypeId( Type::TYPE_SIMPLE )->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') + $productAttributeSetId )->setWebsiteIds( [1] )->setName( @@ -92,6 +117,7 @@ )->setStockData( ['use_config_manage_stock' => 1, 'qty' => 5, 'is_in_stock' => 1] )->save(); + Bootstrap::getObjectManager()->get( Action::class )->updateAttributes( @@ -99,6 +125,7 @@ [ $selectAttributes[1]->getAttributeCode() => $option->getId(), $selectAttributes[2]->getAttributeCode() => $selectOptions[2]->getLastItem()->getId(), + $dateAttribute->getAttributeCode() => '10/30/2000', ], $product->getStoreId() ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php index 18a5372d06d98..fd413726b2637 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php @@ -13,6 +13,7 @@ $registry = Bootstrap::getObjectManager()->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); + /** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $productCollection = Bootstrap::getObjectManager() ->create(Product::class) @@ -20,17 +21,26 @@ foreach ($productCollection as $product) { $product->delete(); } + /** @var $attribute Attribute */ $attribute = Bootstrap::getObjectManager()->create( Attribute::class ); /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); foreach (range(1, 2) as $index) { - $attribute->loadByCode($installer->getEntityTypeId('catalog_product'), 'select_attribute_' . $index); + $attribute->loadByCode($productEntityTypeId, 'select_attribute_' . $index); if ($attribute->getId()) { $attribute->delete(); } } +$attribute->loadByCode($productEntityTypeId, 'date_attribute'); +if ($attribute->getId()) { + $attribute->delete(); +} + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php index 8586f47a0f7fa..0aaa3f4e15bda 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php @@ -35,6 +35,7 @@ 'match_query' => [ 'value' => '$match_term_override$', 'name' => 'match_query', + 'boost' => '1', 'match' => [ 0 => [ 'field' => 'match_field', @@ -50,6 +51,7 @@ ], 'must_query' => [ 'name' => 'must_query', + 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'must', @@ -60,6 +62,7 @@ ], 'should_query' => [ 'name' => 'should_query', + 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'should', @@ -70,6 +73,7 @@ ], 'not_query' => [ 'name' => 'not_query', + 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'not', @@ -80,6 +84,7 @@ ], 'match_query_2' => [ 'value' => '$match_term_override$', + 'boost' => '1', 'name' => 'match_query_2', 'match' => [ 0 => [ @@ -163,6 +168,7 @@ 'queries' => [ 'filter_query' => [ 'name' => 'filter_query', + 'boost' => '1', 'filterReference' => [ 0 => [ @@ -230,6 +236,7 @@ 'new_match_query' => [ 'value' => '$match_term$', 'name' => 'new_match_query', + 'boost' => '1', 'match' => [ 0 => [ diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php new file mode 100644 index 0000000000000..9968704517ecd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple_product'); + +/** @var TaxClassCollectionFactory $taxClassCollectionFactory */ +$taxClassCollectionFactory = $objectManager->get(TaxClassCollectionFactory::class); +$taxClassCollection = $taxClassCollectionFactory->create(); + +/** @var TaxClassModel $taxClass */ +$taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); +$taxClass = $taxClassCollection->getFirstItem(); + +$product->setCustomAttribute('tax_class_id', $taxClass->getClassId()); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php new file mode 100644 index 0000000000000..f465f482275c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$product = $productRepository->get('simple_product'); +$extensionAttributes = $product->getExtensionAttributes(); +$stockItem = $extensionAttributes->getStockItem(); +$stockItem->setIsInStock(false); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product.php new file mode 100644 index 0000000000000..732c18d4d7340 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Api\DataObjectHelper; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::ATTRIBUTE_SET_ID => 4, + ProductInterface::SKU => 'simple_product', + ProductInterface::NAME => 'Simple Product', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_ENABLED, +]; +$dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class); +/** Out of interface */ +$product + ->setWebsiteIds([1]) + ->setStockData([ + 'qty' => 85.5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'is_qty_decimal' => true + ]); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_rollback.php new file mode 100644 index 0000000000000..9a54f663c9c13 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$currentArea = $registry->registry('isSecureArea'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple_product'); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', $currentArea); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product.php new file mode 100644 index 0000000000000..e4472464e17ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Api\DataObjectHelper; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_VIRTUAL, + ProductInterface::ATTRIBUTE_SET_ID => 4, + ProductInterface::SKU => 'virtual_product', + ProductInterface::NAME => 'Virtual Product', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_ENABLED, +]; +$dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class); +/** Out of interface */ +$product + ->setWebsiteIds([1]) + ->setStockData([ + 'qty' => 85.5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'is_qty_decimal' => true + ]); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product_rollback.php new file mode 100644 index 0000000000000..f8d329f574626 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$currentArea = $registry->registry('isSecureArea'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('virtual_product'); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', $currentArea); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php index 16a15cfcd2e26..d0d746812ec44 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php @@ -38,6 +38,9 @@ class GraphQlControllerTest extends \Magento\TestFramework\Indexer\TestCase /** @var MetadataPool */ private $metadataPool; + /** @var Http */ + private $request; + public static function setUpBeforeClass() { $db = Bootstrap::getInstance()->getBootstrap() @@ -57,6 +60,7 @@ protected function setUp() : void $this->graphql = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); $this->jsonSerializer = $this->objectManager->get(SerializerInterface::class); $this->metadataPool = $this->objectManager->get(MetadataPool::class); + $this->request = $this->objectManager->get(Http::class); } /** @@ -86,27 +90,120 @@ public function testDispatch() : void } QUERY; $postData = [ - 'query' => $query, - 'variables' => null, + 'query' => $query, + 'variables' => null, 'operationName' => null ]; - /** @var Http $request */ - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); - $request->setPathInfo('/graphql'); - $request->setContent(json_encode($postData)); + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('POST'); + $this->request->setContent(json_encode($postData)); $headers = $this->objectManager->create(\Zend\Http\Headers::class) ->addHeaders(['Content-Type' => 'application/json']); - $request->setHeaders($headers); - $response = $this->graphql->dispatch($request); + $this->request->setHeaders($headers); + $response = $this->graphql->dispatch($this->request); $output = $this->jsonSerializer->unserialize($response->getContent()); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $this->assertArrayNotHasKey('errors', $output, 'Response has errors'); - $this->assertTrue(!empty($output['data']['products']['items']), 'Products array has items'); - $this->assertTrue(!empty($output['data']['products']['items'][0]), 'Products array has items'); - $this->assertEquals($output['data']['products']['items'][0]['id'], $product->getData($linkField)); - $this->assertEquals($output['data']['products']['items'][0]['sku'], $product->getSku()); - $this->assertEquals($output['data']['products']['items'][0]['name'], $product->getName()); + $this->assertNotEmpty($output['data']['products']['items'], 'Products array has items'); + $this->assertNotEmpty($output['data']['products']['items'][0], 'Products array has items'); + $this->assertEquals($product->getData($linkField), $output['data']['products']['items'][0]['id']); + $this->assertEquals($product->getSku(), $output['data']['products']['items'][0]['sku']); + $this->assertEquals($product->getName(), $output['data']['products']['items'][0]['name']); + } + + /** + * Test request is dispatched and response generated when using GET request with query string + * + * @return void + */ + public function testDispatchWithGet() : void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + /** @var ProductInterface $product */ + $product = $productRepository->get('simple1'); + + $query + = <<<QUERY + { + products(filter: {sku: {eq: "simple1"}}) + { + items { + id + name + sku + } + } + } +QUERY; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphql->dispatch($this->request); + $output = $this->jsonSerializer->unserialize($response->getContent()); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + $this->assertArrayNotHasKey('errors', $output, 'Response has errors'); + $this->assertNotEmpty($output['data']['products']['items'], 'Products array has items'); + $this->assertNotEmpty($output['data']['products']['items'][0], 'Products array has items'); + $this->assertEquals($product->getData($linkField), $output['data']['products']['items'][0]['id']); + $this->assertEquals($product->getSku(), $output['data']['products']['items'][0]['sku']); + $this->assertEquals($product->getName(), $output['data']['products']['items'][0]['name']); + } + + /** Test request is dispatched and response generated when using GET request with parameterized query string + * + * @return void + */ + public function testDispatchGetWithParameterizedVariables() : void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + /** @var ProductInterface $product */ + $product = $productRepository->get('simple1'); + $query = <<<QUERY +query GetProducts(\$filterInput:ProductFilterInput){ + products( + filter:\$filterInput + ){ + items{ + id + name + sku + } + } +} +QUERY; + + $variables = [ + 'filterInput' => [ + 'sku' => ['eq' => 'simple1'] + ] + ]; + $queryParams = [ + 'query' => $query, + 'variables' => json_encode($variables), + 'operationName' => 'GetProducts' + ]; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setParams($queryParams); + $response = $this->graphql->dispatch($this->request); + $output = $this->jsonSerializer->unserialize($response->getContent()); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + $this->assertArrayNotHasKey('errors', $output, 'Response has errors'); + $this->assertNotEmpty($output['data']['products']['items'], 'Products array has items'); + $this->assertNotEmpty($output['data']['products']['items'][0], 'Products array has items'); + $this->assertEquals($product->getData($linkField), $output['data']['products']['items'][0]['id']); + $this->assertEquals($product->getSku(), $output['data']['products']['items'][0]['sku']); + $this->assertEquals($product->getName(), $output['data']['products']['items'][0]['name']); } /** @@ -116,7 +213,6 @@ public function testDispatch() : void */ public function testError() : void { - $this->markTestSkipped('Causes failiure with php unit and php 7.2'); $query = <<<QUERY { @@ -137,25 +233,25 @@ public function testError() : void QUERY; $postData = [ - 'query' => $query, - 'variables' => null, + 'query' => $query, + 'variables' => null, 'operationName' => null ]; - /** @var Http $request */ - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); - $request->setPathInfo('/graphql'); - $request->setContent(json_encode($postData)); + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('POST'); + $this->request->setContent(json_encode($postData)); $headers = $this->objectManager->create(\Zend\Http\Headers::class) ->addHeaders(['Content-Type' => 'application/json']); - $request->setHeaders($headers); - $response = $this->graphql->dispatch($request); + $this->request->setHeaders($headers); + $response = $this->graphql->dispatch($this->request); $outputResponse = $this->jsonSerializer->unserialize($response->getContent()); if (isset($outputResponse['errors'][0])) { if (is_array($outputResponse['errors'][0])) { foreach ($outputResponse['errors'] as $error) { $this->assertEquals( - $error['category'], - \Magento\Framework\GraphQl\Exception\GraphQlInputException::EXCEPTION_CATEGORY + \Magento\Framework\GraphQl\Exception\GraphQlInputException::EXCEPTION_CATEGORY, + $error['category'] ); if (isset($error['message'])) { $this->assertEquals($error['message'], 'Invalid entity_type specified: invalid'); @@ -169,12 +265,4 @@ public function testError() : void } } } - - /** - * teardown - */ - public function tearDown() - { - parent::tearDown(); - } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php new file mode 100644 index 0000000000000..f62b463d94003 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('simple_product'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php new file mode 100644 index 0000000000000..f597d0bffa7ce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('virtual-product'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon.php new file mode 100644 index 0000000000000..c70efa9a12a5d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CouponManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponManagementInterface $couponManagement */ +$couponManagement = Bootstrap::getObjectManager()->get(CouponManagementInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$couponManagement->set($quote->getId(), '2?ds5!2d'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon_rollback.php new file mode 100644 index 0000000000000..5431c25b7df53 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon_rollback.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CouponManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponManagementInterface $couponManagement */ +$couponManagement = Bootstrap::getObjectManager()->get(CouponManagementInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$couponManagement->remove($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php new file mode 100644 index 0000000000000..86174a7753f4e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CartManagementInterface $cartManagement */ +$cartManagement = Bootstrap::getObjectManager()->get(CartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$cartId = $cartManagement->createEmptyCartForCustomer(1); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('test_quote'); +$cartRepository->save($cart); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($cartId) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php new file mode 100644 index 0000000000000..0d5d5f067e35a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php new file mode 100644 index 0000000000000..8e6d4b8f74b86 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$paymentMethodList = $objectManager->get(\Magento\Payment\Api\PaymentMethodListInterface::class); +$rollbackConfigKey = 'test/payment/disabled_payment_methods'; +$configData = []; +$disabledPaymentMethods = []; + +// Get all active Payment Methods +foreach ($paymentMethodList->getActiveList(Store::DEFAULT_STORE_ID) as $paymentMethod) { + $configData['payment/' . $paymentMethod->getCode() . '/active'] = 0; + $disabledPaymentMethods[] = $paymentMethod->getCode(); +} +// Remember all manually disabled Payment Methods for rollback +$configData[$rollbackConfigKey] = implode(',', $disabledPaymentMethods); + +/** @var Config $defConfig */ +$defConfig = $objectManager->create(Config::class); +$defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + +foreach ($configData as $key => $value) { + $defConfig->setDataByPath($key, $value); + $defConfig->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php new file mode 100644 index 0000000000000..092826d1fd3f7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$rollbackConfigKey = 'test/payment/disabled_payment_methods'; + +$configWriter = $objectManager->create(WriterInterface::class); +$rollbackConfigValue = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class) + ->getStore(\Magento\Store\Model\Store::DEFAULT_STORE_ID) + ->getConfig($rollbackConfigKey); + +$disabledPaymentMethods = []; +if (!empty($rollbackConfigValue)) { + $disabledPaymentMethods = explode(',', $rollbackConfigValue); +} + +if (count($disabledPaymentMethods)) { + foreach ($disabledPaymentMethods as $keyToRemove) { + $configWriter->delete(sprintf('payment/%s/active', $keyToRemove)); + } +} +$configWriter->delete($rollbackConfigKey); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php new file mode 100644 index 0000000000000..c5d0468dbfacb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/flatrate/active', 0); +$configWriter->save('carriers/tablerate/active', 0); +$configWriter->save('carriers/freeshipping/active', 0); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods_rollback.php new file mode 100644 index 0000000000000..384ffbdf51f3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods_rollback.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/flatrate/active'); +$configWriter->delete('carriers/tablerate/active'); +$configWriter->delete('carriers/freeshipping/active'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php new file mode 100644 index 0000000000000..9c15589ba82e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('payment/banktransfer/active', 1); +$configWriter->save('payment/cashondelivery/active', 1); +$configWriter->save('payment/checkmo/active', 1); +$configWriter->save('payment/purchaseorder/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php new file mode 100644 index 0000000000000..61b7ed9737ff9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('payment/banktransfer/active'); +$configWriter->delete('payment/cashondelivery/active'); +$configWriter->delete('payment/checkmo/active'); +$configWriter->delete('payment/purchaseorder/active'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php new file mode 100644 index 0000000000000..ebc41da9b1b3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/flatrate/active', 1); +$configWriter->save('carriers/tablerate/active', 1); +$configWriter->save('carriers/freeshipping/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php new file mode 100644 index 0000000000000..384ffbdf51f3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/flatrate/active'); +$configWriter->delete('carriers/tablerate/active'); +$configWriter->delete('carriers/freeshipping/active'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php new file mode 100644 index 0000000000000..6a9ed898c3161 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var GuestCartManagementInterface $guestCartManagement */ +$guestCartManagement = Bootstrap::getObjectManager()->get(GuestCartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId */ +$maskedQuoteIdToQuoteId = Bootstrap::getObjectManager()->get(MaskedQuoteIdToQuoteIdInterface::class); + +$cartHash = $guestCartManagement->createEmptyCart(); +$cartId = $maskedQuoteIdToQuoteId->execute($cartHash); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('test_quote'); +$cartRepository->save($cart); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php new file mode 100644 index 0000000000000..0d5d5f067e35a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address.php new file mode 100644 index 0000000000000..60d2f1c49d240 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +/** @var GuestCartManagementInterface $guestCartManagement */ +$guestCartManagement = Bootstrap::getObjectManager()->get(GuestCartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId */ +$maskedQuoteIdToQuoteId = Bootstrap::getObjectManager()->get(MaskedQuoteIdToQuoteIdInterface::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$cartHash = $guestCartManagement->createEmptyCart(); +$cartId = $maskedQuoteIdToQuoteId->execute($cartHash); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('guest_quote_with_address'); +$cartRepository->save($cart); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 4435555, + AddressInterface::KEY_POSTCODE => 78717, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityA', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Andora str, 121', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); +$shippingAddressManagement->assign($cartId, $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address_rollback.php new file mode 100644 index 0000000000000..d9f894abf45b4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'guest_quote_with_address', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/set_guest_email.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/set_guest_email.php new file mode 100644 index 0000000000000..c8084b2552395 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/set_guest_email.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + +$quote->setCustomerEmail('guest@example.com'); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php new file mode 100644 index 0000000000000..b5704f82879b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->setIsActive(false); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired.php new file mode 100644 index 0000000000000..5316b184ecd15 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Model\CouponFactory; +use Magento\SalesRule\Model\Spi\CouponResourceInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponResourceInterface $couponResource */ +$couponResource = Bootstrap::getObjectManager()->get(CouponResourceInterface::class); +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); + +$coupon = $couponFactory->create(); +$coupon->loadByCode('2?ds5!2d'); +$yesterday = new \DateTime(); +$yesterday->add(\DateInterval::createFromDateString('-1 day')); +$coupon->setExpirationDate($yesterday->format('Y-m-d')); +$couponResource->save($coupon); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired_rollback.php new file mode 100644 index 0000000000000..32c3d78bafd09 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Model\CouponFactory; +use Magento\SalesRule\Model\Spi\CouponResourceInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponResourceInterface $couponResource */ +$couponResource = Bootstrap::getObjectManager()->get(CouponResourceInterface::class); +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); + +$coupon = $couponFactory->create(); +$coupon->loadByCode('2?ds5!2d'); + +if ($coupon->getId()) { + $coupon->setExpirationDate(null); + $couponResource->save($coupon); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php new file mode 100644 index 0000000000000..e58c6b21d8d23 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Api\Data\ConditionInterface; +use Magento\SalesRule\Api\Data\ConditionInterfaceFactory; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Model\CouponFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); +/** @var ConditionInterfaceFactory $conditionFactory */ +$conditionFactory = Bootstrap::getObjectManager()->get(ConditionInterfaceFactory::class); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); + +$couponCode = '2?ds5!2d'; +$sku = 'simple_product'; + +$coupon = $couponFactory->create(); +$coupon->loadByCode($couponCode); +$ruleId = $coupon->getRuleId(); +$salesRule = $ruleRepository->getById($ruleId); + +/** @var ConditionInterface $conditionProductSku */ +$conditionProductSku = $conditionFactory->create(); +$conditionProductSku->setConditionType(\Magento\SalesRule\Model\Rule\Condition\Product::class); +$conditionProductSku->setAttributeName('sku'); +$conditionProductSku->setValue('1'); +$conditionProductSku->setOperator('!='); +$conditionProductSku->setValue($sku); + +/** @var ConditionInterface $conditionProductFound */ +$conditionProductFound = $conditionFactory->create(); +$conditionProductFound->setConditionType(\Magento\SalesRule\Model\Rule\Condition\Product\Found::class); +$conditionProductFound->setValue('1'); +$conditionProductFound->setAggregatorType('all'); +$conditionProductFound->setConditions([$conditionProductSku]); + +/** @var ConditionInterface $conditionCombine */ +$conditionCombine = $conditionFactory->create(); +$conditionCombine->setConditionType(\Magento\SalesRule\Model\Rule\Condition\Combine::class); +$conditionCombine->setValue('1'); +$conditionCombine->setAggregatorType('all'); +$conditionCombine->setConditions([$conditionProductFound]); + +$salesRule->setCondition($conditionCombine); +$ruleRepository->save($salesRule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product_rollback.php new file mode 100644 index 0000000000000..86ab253f1d3c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product_rollback.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Api\Data\ConditionInterface; +use Magento\SalesRule\Api\Data\ConditionInterfaceFactory; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Model\CouponFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); +/** @var ConditionInterfaceFactory $conditionFactory */ +$conditionFactory = Bootstrap::getObjectManager()->get(ConditionInterfaceFactory::class); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); + +$couponCode = '2?ds5!2d'; +$sku = 'simple_product'; + +$coupon = $couponFactory->create(); +$coupon->loadByCode($couponCode); + +if ($coupon->getId()) { + $ruleId = $coupon->getRuleId(); + $salesRule = $ruleRepository->getById($ruleId); + + /** @var ConditionInterface $conditionCombine */ + $conditionCombine = $conditionFactory->create(); + $conditionCombine->setConditions([]); + + $salesRule->setCondition($conditionCombine); + $ruleRepository->save($salesRule); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php new file mode 100644 index 0000000000000..a973a982b1150 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\OfflinePayments\Model\Checkmo; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Api\Data\PaymentInterfaceFactory; +use Magento\Quote\Api\PaymentMethodManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var PaymentInterfaceFactory $paymentFactory */ +$paymentFactory = Bootstrap::getObjectManager()->get(PaymentInterfaceFactory::class); +/** @var PaymentMethodManagementInterface $paymentMethodManagement */ +$paymentMethodManagement = Bootstrap::getObjectManager()->get(PaymentMethodManagementInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + +$payment = $paymentFactory->create([ + 'data' => [ + PaymentInterface::KEY_METHOD => Checkmo::PAYMENT_METHOD_CHECKMO_CODE, + ] +]); +$paymentMethodManagement->set($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php new file mode 100644 index 0000000000000..1e7c10d251be0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var ShippingInformationInterfaceFactory $shippingInformationFactory */ +$shippingInformationFactory = Bootstrap::getObjectManager()->get(ShippingInformationInterfaceFactory::class); +/** @var ShippingInformationManagementInterface $shippingInformationManagement */ +$shippingInformationManagement = Bootstrap::getObjectManager()->get(ShippingInformationManagementInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteAddress = $quote->getShippingAddress(); + +/** @var ShippingInformationInterface $shippingInformation */ +$shippingInformation = $shippingInformationFactory->create([ + 'data' => [ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => 'flatrate', + ShippingInformationInterface::SHIPPING_METHOD_CODE => 'flatrate', + ], +]); +$shippingInformationManagement->saveAddressInformation($quote->getId(), $shippingInformation); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_billing_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_billing_address.php new file mode 100644 index 0000000000000..2e15ef218d0c5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_billing_address.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\BillingAddressManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var BillingAddressManagementInterface $billingAddressManagement */ +$billingAddressManagement = Bootstrap::getObjectManager()->get(BillingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 75477, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$billingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php new file mode 100644 index 0000000000000..54f4d8d0c6e75 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => '75477', + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$shippingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php new file mode 100644 index 0000000000000..8e60dc904bd4e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 'M4L 1V3', + AddressInterface::KEY_COUNTRY_ID => 'CA', + AddressInterface::KEY_CITY => 'Toronto', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => '500 Kingston Rd', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_CODE => 'ON', +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$shippingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php new file mode 100644 index 0000000000000..aca55bd8414f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\Data\TaxRateInterface; +use Magento\Tax\Api\Data\TaxRuleInterface; +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var Rate $rate */ +$rate = $rateFactory->create(); +$rateData = [ + Rate::KEY_COUNTRY_ID => 'US', + Rate::KEY_REGION_ID => '1', + Rate::KEY_POSTCODE => '*', + Rate::KEY_CODE => 'US-TEST-*-Rate-1', + Rate::KEY_PERCENTAGE_RATE => '7.5', +]; +$dataObjectHelper->populateWithArray($rate, $rateData, TaxRateInterface::class); +$rateRepository->save($rate); + +$rule = $ruleFactory->create(); +$ruleData = [ + Rule::KEY_CODE=> 'GraphQl Test Rule', + Rule::KEY_PRIORITY => '0', + Rule::KEY_POSITION => '0', + Rule::KEY_CUSTOMER_TAX_CLASS_IDS => [3], + Rule::KEY_PRODUCT_TAX_CLASS_IDS => [2], + Rule::KEY_TAX_RATE_IDS => [$rate->getId()], +]; +$dataObjectHelper->populateWithArray($rule, $ruleData, TaxRuleInterface::class); +$ruleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1_rollback.php new file mode 100644 index 0000000000000..aba1960624ed4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tax\Model\ResourceModel\Calculation\Rate as RateResource; +use Magento\Tax\Model\ResourceModel\Calculation\Rule as RuleResource; + +$objectManager = Bootstrap::getObjectManager(); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var RateResource $rateResource */ +$rateResource = $objectManager->get(RateResource::class); +/** @var RuleResource $ruleResource */ +$ruleResource = $objectManager->get(RuleResource::class); + +$rate = $rateFactory->create(); +$rateResource->load($rate, 'US-TEST-*-Rate-1', Rate::KEY_CODE); +$rule = $ruleFactory->create(); +$ruleResource->load($rule, 'GraphQl Test Rule', Rule::KEY_CODE); +$ruleRepository->delete($rule); +$rateRepository->delete($rate); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php new file mode 100644 index 0000000000000..42931db75a433 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/ups/active', 1); +$configWriter->save('carriers/ups/type', "UPS"); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method_rollback.php new file mode 100644 index 0000000000000..cf6dc08dd91a4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/ups/active'); +$configWriter->delete('carriers/ups/type'); diff --git a/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php b/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php index 67817b068ff09..afd515757ae4b 100644 --- a/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php +++ b/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php @@ -9,7 +9,10 @@ class GroupedTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ 'grouped-product' => [ @@ -23,17 +26,13 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { $expectedAssociatedProducts = $expectedProduct->getTypeInstance()->getAssociatedProducts($expectedProduct); $actualAssociatedProducts = $actualProduct->getTypeInstance()->getAssociatedProducts($actualProduct); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php index 9afce0ed10bcd..a3cf42b48489f 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ImportExport\Controller\Adminhtml\Import; use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; @@ -25,7 +27,7 @@ class ValidateTest extends \Magento\TestFramework\TestCase\AbstractBackendContro * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.Superglobals) */ - public function testValidationReturn(string $fileName, string $mimeType, string $message, string $delimiter) + public function testValidationReturn(string $fileName, string $mimeType, string $message, string $delimiter): void { $validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR; @@ -62,10 +64,7 @@ public function testValidationReturn(string $fileName, string $mimeType, string $this->_objectManager->configure( [ - 'preferences' => [ - \Magento\Framework\HTTP\Adapter\FileTransferFactory::class => - \Magento\ImportExport\Controller\Adminhtml\Import\HttpFactoryMock::class - ] + 'preferences' => [FileTransferFactory::class => HttpFactoryMock::class] ] ); @@ -82,7 +81,7 @@ public function testValidationReturn(string $fileName, string $mimeType, string /** * @return array */ - public function validationDataProvider() + public function validationDataProvider(): array { return [ [ diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index 3fa80a2dcda1a..2eda32e894d3c 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -109,20 +109,6 @@ protected function setUp() }); } - /** - * Checks that pid files are created - * - * @return void - */ - public function testCheckThatPidFilesWasCreated() - { - $this->markTestSkipped('MC-5904: Test Fails randomly,'); - $this->consumersRunner->run(); - foreach ($this->consumerConfig->getConsumers() as $consumer) { - $this->waitConsumerPidFile($consumer->getName()); - } - } - /** * Tests running of specific consumer and his re-running when it is working * @@ -130,8 +116,6 @@ public function testCheckThatPidFilesWasCreated() */ public function testSpecificConsumerAndRerun() { - $this->markTestSkipped('MC-5904: Test Fails randomly,'); - $specificConsumer = 'quoteItemCleaner'; $pidFilePath = $this->getPidFileName($specificConsumer); $config = $this->config; @@ -188,23 +172,6 @@ public function testCronJobDisabled() } } - /** - * @param string $consumerName - * @return void - */ - private function waitConsumerPidFile($consumerName) - { - $pidFileFullPath = $this->getPidFileFullPath($consumerName); - $i = 0; - do { - sleep(1); - } while (!file_exists($pidFileFullPath) && ($i++ < 60)); - - if (!file_exists($pidFileFullPath)) { - $this->fail($consumerName . ' pid file does not exist.'); - } - } - /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObject.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObject.php deleted file mode 100644 index 31843be00a54b..0000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObject.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MysqlMq\Model; - -class DataObject extends \Magento\Framework\Api\AbstractExtensibleObject -{ - /** - * @return string - */ - public function getName() - { - return $this->_get('name'); - } - - /** - * @param string $name - * @return $this - */ - public function setName($name) - { - return $this->setData('name', $name); - } - - /** - * @return int|null - */ - public function getEntityId() - { - return $this->_get('entity_id'); - } - - /** - * @param int $entityId - * @return $this - */ - public function setEntityId($entityId) - { - return $this->setData('entity_id', $entityId); - } -} diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObjectRepository.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObjectRepository.php deleted file mode 100644 index 879872315ec51..0000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObjectRepository.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MysqlMq\Model; - -class DataObjectRepository -{ - /** - * @param DataObject $dataObject - * @param string $requiredParam - * @param int|null $optionalParam - * @return bool - */ - public function delayedOperation( - \Magento\MysqlMq\Model\DataObject $dataObject, - $requiredParam, - $optionalParam = null - ) { - echo "Processed '{$dataObject->getEntityId()}'; " - . "Required param '{$requiredParam}'; Optional param '{$optionalParam}'\n"; - return true; - } -} diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Processor.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Processor.php deleted file mode 100644 index 3b2a76104a2cd..0000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Processor.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MysqlMq\Model; - -/** - * Test message processor is used by \Magento\MysqlMq\Model\PublisherTest - */ -class Processor -{ - /** - * @param \Magento\MysqlMq\Model\DataObject $message - */ - public function processMessage($message) - { - echo "Processed {$message->getEntityId()}\n"; - } - - /** - * @param \Magento\MysqlMq\Model\DataObject $message - */ - public function processMessageWithException($message) - { - throw new \LogicException("Exception during message processing happened. Entity: {{$message->getEntityId()}}"); - } -} diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php index f03d03d3a25fd..f911165bd27fb 100644 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php +++ b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php @@ -3,87 +3,45 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\MysqlMq\Model; -use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\MessageQueue\UseCase\QueueTestCaseAbstract; +use Magento\MysqlMq\Model\ResourceModel\MessageCollection; +use Magento\MysqlMq\Model\ResourceModel\MessageStatusCollection; /** * Test for MySQL publisher class. * * @magentoDbIsolation disabled */ -class PublisherConsumerTest extends \PHPUnit\Framework\TestCase +class PublisherConsumerTest extends QueueTestCaseAbstract { const MAX_NUMBER_OF_TRIALS = 3; /** - * @var \Magento\Framework\MessageQueue\PublisherInterface + * @var string[] */ - protected $publisher; - - /** - * @var \Magento\Framework\ObjectManagerInterface - */ - protected $objectManager; - - protected function setUp() - { - $this->markTestIncomplete('Should be converted to queue config v2.'); - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $configPath = __DIR__ . '/../etc/queue.xml'; - $fileResolverMock = $this->createMock(\Magento\Framework\Config\FileResolverInterface::class); - $fileResolverMock->expects($this->any()) - ->method('get') - ->willReturn([$configPath => file_get_contents(($configPath))]); - - /** @var \Magento\Framework\MessageQueue\Config\Reader\Xml $xmlReader */ - $xmlReader = $this->objectManager->create( - \Magento\Framework\MessageQueue\Config\Reader\Xml::class, - ['fileResolver' => $fileResolverMock] - ); - - $newData = $xmlReader->read(); - - /** @var \Magento\Framework\MessageQueue\Config\Data $configData */ - $configData = $this->objectManager->get(\Magento\Framework\MessageQueue\Config\Data::class); - $configData->reset(); - $configData->merge($newData); - - $this->publisher = $this->objectManager->create(\Magento\Framework\MessageQueue\PublisherInterface::class); - } - - protected function tearDown() - { - $this->markTestIncomplete('Should be converted to queue config v2.'); - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueTwo', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueThree', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueFour', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueFive', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueOneWithException', PHP_INT_MAX); - - $objectManagerConfiguration = [\Magento\Framework\MessageQueue\Config\Reader\Xml::class => [ - 'arguments' => [ - 'fileResolver' => ['instance' => \Magento\Framework\Config\FileResolverInterface::class], - ], - ], - ]; - $this->objectManager->configure($objectManagerConfiguration); - /** @var \Magento\Framework\MessageQueue\Config\Data $queueConfig */ - $queueConfig = $this->objectManager->get(\Magento\Framework\MessageQueue\Config\Data::class); - $queueConfig->reset(); - } + protected $consumers = [ + 'demoConsumerQueueOne', + 'demoConsumerQueueTwo', + 'demoConsumerQueueThree', + 'delayedOperationConsumer', + 'demoConsumerWithException' + ]; /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ public function testPublishConsumeFlow() { - /** @var \Magento\MysqlMq\Model\DataObjectFactory $objectFactory */ - $objectFactory = $this->objectManager->create(\Magento\MysqlMq\Model\DataObjectFactory::class); - /** @var \Magento\MysqlMq\Model\DataObject $object */ + /** @var \Magento\TestModuleMysqlMq\Model\DataObjectFactory $objectFactory */ + $objectFactory = $this->objectManager->create(\Magento\TestModuleMysqlMq\Model\DataObjectFactory::class); + /** @var \Magento\TestModuleMysqlMq\Model\DataObject $object */ $object = $objectFactory->create(); + $object->setOutputPath($this->logFilePath); + file_put_contents($this->logFilePath, ''); for ($i = 0; $i < 10; $i++) { $object->setName('Object name ' . $i)->setEntityId($i); $this->publisher->publish('demo.object.created', $object); @@ -96,105 +54,87 @@ public function testPublishConsumeFlow() $object->setName('Object name ' . $i)->setEntityId($i); $this->publisher->publish('demo.object.custom.created', $object); } - - $outputPattern = '/(Processed \d+\s)/'; - /** There are total of 10 messages in the first queue, total expected consumption is 7, 3 then 0 */ - $this->consumeMessages('demoConsumerQueueOne', 7, 7, $outputPattern); - /** Consumer all messages which left in this queue */ - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX, 3, $outputPattern); - $this->consumeMessages('demoConsumerQueueOne', 7, 0, $outputPattern); - - /** Verify that messages were added correctly to second queue for update and create topics */ - $this->consumeMessages('demoConsumerQueueTwo', 20, 15, $outputPattern); - - /** Verify that messages were NOT added to fourth queue */ - $this->consumeMessages('demoConsumerQueueFour', 11, 0, $outputPattern); - - /** Verify that messages were added correctly by '*' pattern in bind config to third queue */ - $this->consumeMessages('demoConsumerQueueThree', 20, 15, $outputPattern); - - /** Verify that messages were added correctly by '#' pattern in bind config to fifth queue */ - $this->consumeMessages('demoConsumerQueueFive', 20, 18, $outputPattern); + $this->waitForAsynchronousResult(18, $this->logFilePath); + + //Check lines in file + $createdPattern = '/Processed object created \d+/'; + $updatedPattern = '/Processed object updated \d+/'; + $customCreatedPattern = '/Processed custom object created \d+/'; + $logFileContents = file_get_contents($this->logFilePath); + + preg_match_all($createdPattern, $logFileContents, $createdMatches); + $this->assertEquals(10, count($createdMatches[0])); + preg_match_all($updatedPattern, $logFileContents, $updatedMatches); + $this->assertEquals(5, count($updatedMatches[0])); + preg_match_all($customCreatedPattern, $logFileContents, $customCreatedMatches); + $this->assertEquals(3, count($customCreatedMatches[0])); } /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ - public function testPublishAndConsumeWithFailedJobs() + public function testPublishAndConsumeSchemaDefinedByMethod() { - /** @var \Magento\MysqlMq\Model\DataObjectFactory $objectFactory */ - $objectFactory = $this->objectManager->create(\Magento\MysqlMq\Model\DataObjectFactory::class); - /** @var \Magento\MysqlMq\Model\DataObject $object */ - /** Try consume messages for MAX_NUMBER_OF_TRIALS and then consumer them without exception */ + $topic = 'test.schema.defined.by.method'; + /** @var \Magento\TestModuleMysqlMq\Model\DataObjectFactory $objectFactory */ + $objectFactory = $this->objectManager->create(\Magento\TestModuleMysqlMq\Model\DataObjectFactory::class); + /** @var \Magento\TestModuleMysqlMq\Model\DataObject $object */ $object = $objectFactory->create(); - for ($i = 0; $i < 5; $i++) { - $object->setName('Object name ' . $i)->setEntityId($i); - $this->publisher->publish('demo.object.created', $object); - } - $outputPattern = '/(Processed \d+\s)/'; - for ($i = 0; $i < self::MAX_NUMBER_OF_TRIALS; $i++) { - $this->consumeMessages('demoConsumerQueueOneWithException', PHP_INT_MAX, 0, $outputPattern); - } - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX, 0, $outputPattern); + $id = 33; + $object->setName('Object name ' . $id)->setEntityId($id); + $object->setOutputPath($this->logFilePath); + $requiredStringParam = 'Required value'; + $optionalIntParam = 44; + $this->publisher->publish($topic, [$object, $requiredStringParam, $optionalIntParam]); - /** Try consume messages for MAX_NUMBER_OF_TRIALS+1 and then consumer them without exception */ - for ($i = 0; $i < 5; $i++) { - $object->setName('Object name ' . $i)->setEntityId($i); - $this->publisher->publish('demo.object.created', $object); - } - /** Try consume messages for MAX_NUMBER_OF_TRIALS and then consumer them without exception */ - for ($i = 0; $i < self::MAX_NUMBER_OF_TRIALS + 1; $i++) { - $this->consumeMessages('demoConsumerQueueOneWithException', PHP_INT_MAX, 0, $outputPattern); - } - /** Make sure that messages are not accessible anymore after number of trials is exceeded */ - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX, 0, $outputPattern); + $expectedOutput = "Processed '{$object->getEntityId()}'; " + . "Required param '{$requiredStringParam}'; Optional param '{$optionalIntParam}'"; + + $this->waitForAsynchronousResult(1, $this->logFilePath); + + $this->assertEquals($expectedOutput, trim(file_get_contents($this->logFilePath))); } /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ - public function testPublishAndConsumeSchemaDefinedByMethod() + public function testConsumeWithException() { - /** @var \Magento\MysqlMq\Model\DataObjectFactory $objectFactory */ - $objectFactory = $this->objectManager->create(\Magento\MysqlMq\Model\DataObjectFactory::class); - /** @var \Magento\MysqlMq\Model\DataObject $object */ + $topic = 'demo.exception'; + /** @var \Magento\TestModuleMysqlMq\Model\DataObjectFactory $objectFactory */ + $objectFactory = $this->objectManager->create(\Magento\TestModuleMysqlMq\Model\DataObjectFactory::class); + /** @var \Magento\TestModuleMysqlMq\Model\DataObject $object */ $object = $objectFactory->create(); - $id = 33; + $id = 99; + $object->setName('Object name ' . $id)->setEntityId($id); - $requiredStringParam = 'Required value'; - $optionalIntParam = 44; - $this->publisher->publish('test.schema.defined.by.method', [$object, $requiredStringParam, $optionalIntParam]); - $outputPattern = "/Processed '{$object->getEntityId()}'; " - . "Required param '{$requiredStringParam}'; Optional param '{$optionalIntParam}'/"; - $this->consumeMessages('delayedOperationConsumer', PHP_INT_MAX, 1, $outputPattern); + $object->setOutputPath($this->logFilePath); + $this->publisher->publish($topic, $object); + $expectedOutput = "Exception processing {$id}"; + $this->waitForAsynchronousResult(1, $this->logFilePath); + $message = $this->getTopicLatestMessage($topic); + $this->assertEquals($expectedOutput, trim(file_get_contents($this->logFilePath))); + $this->assertEquals(QueueManagement::MESSAGE_STATUS_ERROR, $message->getStatus()); } /** - * Make sure that consumers consume correct number of messages. - * - * @param string $consumerName - * @param int|null $messagesToProcess - * @param int|null $expectedNumberOfProcessedMessages - * @param string|null $outputPattern + * @param string $topic + * @return Message */ - protected function consumeMessages( - $consumerName, - $messagesToProcess, - $expectedNumberOfProcessedMessages = null, - $outputPattern = null - ) { - /** @var \Magento\Framework\MessageQueue\ConsumerFactory $consumerFactory */ - $consumerFactory = $this->objectManager->create(\Magento\Framework\MessageQueue\ConsumerFactory::class); - $consumer = $consumerFactory->get($consumerName); - ob_start(); - $consumer->process($messagesToProcess); - $consumersOutput = ob_get_contents(); - ob_end_clean(); - if ($outputPattern) { - $this->assertEquals( - $expectedNumberOfProcessedMessages, - preg_match_all($outputPattern, $consumersOutput) - ); - } + private function getTopicLatestMessage(string $topic) : Message + { + // Assert message status is error + $messageCollection = $this->objectManager->create(MessageCollection::class); + $messageStatusCollection = $this->objectManager->create(MessageStatusCollection::class); + + $messageCollection->addFilter('topic_name', $topic); + $messageCollection->join( + ['status' => $messageStatusCollection->getMainTable()], + "status.message_id = main_table.id" + ); + $messageCollection->addOrder('updated_at', MessageCollection::SORT_ORDER_DESC); + + $message = $messageCollection->getFirstItem(); + return $message; } } diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php index 197df29233297..56dd77d3da17c 100644 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php @@ -5,8 +5,6 @@ */ namespace Magento\MysqlMq\Model; -use Magento\MysqlMq\Model\QueueManagement; - /** * Test for Queue Management class. */ diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/etc/queue.xml b/dev/tests/integration/testsuite/Magento/MysqlMq/etc/queue.xml deleted file mode 100644 index fd618d504df07..0000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/etc/queue.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?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-message-queue:etc/queue.xsd"> - <publisher name="demo-publisher-1" connection="db" exchange="magento"/> - <publisher name="demo-publisher-2" connection="db" exchange="magento"/> - - <publisher name="test-publisher-1" connection="amqp" exchange="magento"/> - <publisher name="test-publisher-3" connection="amqp" exchange="test-exchange-1"/> - - <topic name="demo.object.created" schema="Magento\MysqlMq\Model\DataObject" publisher="demo-publisher-1"/> - <topic name="demo.object.updated" schema="Magento\MysqlMq\Model\DataObject" publisher="demo-publisher-2"/> - <topic name="demo.object.custom.created" schema="Magento\MysqlMq\Model\DataObject" publisher="demo-publisher-2"/> - - <topic name="test.schema.defined.by.method" schema="Magento\MysqlMq\Model\DataObjectRepository::delayedOperation" publisher="demo-publisher-2"/> - - <topic name="customer.created" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.created.one" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.created.one.two" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.created.two" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.updated" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="demo-publisher-2"/> - <topic name="customer.deleted" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="demo-publisher-2"/> - <topic name="cart.created" schema="Magento\Quote\Api\Data\CartInterface" publisher="test-publisher-3"/> - <topic name="cart.created.one" schema="Magento\Quote\Api\Data\CartInterface" publisher="test-publisher-3"/> - - <consumer name="demoConsumerQueueOne" queue="demo-queue-1" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueOneWithException" queue="demo-queue-1" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessageWithException"/> - <consumer name="demoConsumerQueueTwo" queue="demo-queue-2" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueThree" queue="demo-queue-3" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueFour" queue="demo-queue-4" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueFive" queue="demo-queue-5" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - - <consumer name="customerCreatedListener" queue="test-queue-1" connection="amqp" class="Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="customerDeletedListener" queue="test-queue-2" connection="db" class="Magento\MysqlMq\Model\Processor" method="processMessage" max_messages="98765"/> - <consumer name="cartCreatedListener" queue="test-queue-3" connection="amqp" class="Magento\MysqlMq\Model\Processor" method="processMessage"/> - - <consumer name="delayedOperationConsumer" queue="demo-queue-6" connection="db" class="Magento\MysqlMq\Model\DataObjectRepository" method="delayedOperation"/> - - <bind queue="demo-queue-1" exchange="magento" topic="demo.object.created"/> - <bind queue="demo-queue-2" exchange="magento" topic="demo.object.created"/> - <bind queue="demo-queue-2" exchange="magento" topic="demo.object.updated"/> - <bind queue="demo-queue-3" exchange="magento" topic="demo.object.*"/> - <bind queue="demo-queue-5" exchange="magento" topic="demo.object.#"/> - - <bind queue="demo-queue-6" exchange="magento" topic="test.schema.defined.by.method"/> - - <bind queue="test-queue-1" exchange="magento" topic="customer.created"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.created.one"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.created.one.two"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.created.two"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.updated"/> - <bind queue="test-queue-1" exchange="test-exchange-1" topic="cart.created"/> - <bind queue="test-queue-2" exchange="magento" topic="customer.created"/> - <bind queue="test-queue-2" exchange="magento" topic="customer.deleted"/> - <bind queue="test-queue-3" exchange="magento" topic="cart.created"/> - <bind queue="test-queue-3" exchange="magento" topic="cart.created.one"/> - <bind queue="test-queue-3" exchange="test-exchange-1" topic="cart.created"/> - <bind queue="test-queue-4" exchange="magento" topic="customer.*"/> - <bind queue="test-queue-5" exchange="magento" topic="customer.#"/> - <bind queue="test-queue-7" exchange="magento" topic="*.created.*"/> - <bind queue="test-queue-8" exchange="magento" topic="*.created.#"/> - <bind queue="test-queue-9" exchange="magento" topic="#"/> -</config> diff --git a/dev/tests/integration/testsuite/Magento/NewRelicReporting/Model/Module/CollectTest.php b/dev/tests/integration/testsuite/Magento/NewRelicReporting/Model/Module/CollectTest.php new file mode 100644 index 0000000000000..5e5051163cc1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/NewRelicReporting/Model/Module/CollectTest.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\NewRelicReporting\Model\Module; + +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/*** + * Class CollectTest + */ +class CollectTest extends TestCase +{ + /** + * @var Collect + */ + private $collect; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->collect = Bootstrap::getObjectManager()->create(Collect::class); + } + + /** + * @return void + */ + public function testReport() + { + $this->collect->getModuleData(); + $moduleData = $this->collect->getModuleData(); + $this->assertEmpty($moduleData['changes']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php new file mode 100644 index 0000000000000..48d3356525f49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Block\Adminhtml\Subscriber; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + * @see \Magento\Newsletter\Block\Adminhtml\Subscriber\Grid + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var null|\Magento\Framework\ObjectManagerInterface + */ + private $objectManager = null; + /** + * @var null|\Magento\Framework\View\LayoutInterface + */ + private $layout = null; + + /** + * Set up layout. + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->layout = $this->objectManager->create(\Magento\Framework\View\LayoutInterface::class); + $this->layout->getUpdate()->load('newsletter_subscriber_grid'); + $this->layout->generateXml(); + $this->layout->generateElements(); + } + + /** + * Check if mass action block exists. + */ + public function testMassActionBlockExists() + { + $this->assertNotFalse( + $this->getMassActionBlock(), + 'Mass action block does not exist in the grid, or it name was changed.' + ); + } + + /** + * Check if mass action id field is correct. + */ + public function testMassActionFieldIdIsCorrect() + { + $this->assertEquals( + 'subscriber_id', + $this->getMassActionBlock()->getMassactionIdField(), + 'Mass action id field is incorrect.' + ); + } + + /** + * Check if function returns correct result. + * + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testMassActionBlockContainsCorrectIdList() + { + $this->assertEquals( + implode(',', $this->getAllSubscriberIdList()), + $this->getMassActionBlock()->getGridIdsJson(), + 'Function returns incorrect result.' + ); + } + + /** + * Retrieve mass action block. + * + * @return bool|\Magento\Backend\Block\Widget\Grid\Massaction + */ + private function getMassActionBlock() + { + return $this->layout->getBlock('adminhtml.newslettrer.subscriber.grid.massaction'); + } + + /** + * Retrieve list of id of all subscribers. + * + * @return array + */ + private function getAllSubscriberIdList() + { + /** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ + $resourceConnection = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + $select = $resourceConnection->getConnection() + ->select() + ->from($resourceConnection->getTableName('newsletter_subscriber')) + ->columns(['subscriber_id' => 'subscriber_id']); + + return $resourceConnection->getConnection()->fetchCol($select); + } +} diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight.php new file mode 100644 index 0000000000000..40e01a81ac807 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class); +$connection = $resource->getConnection(); +$resourceModel = $objectManager->create(\Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate::class); +$entityTable = $resourceModel->getTable('shipping_tablerate'); +$data = + [ + 'website_id' => 1, + 'dest_country_id' => 'US', + 'dest_region_id' => 0, + 'dest_zip' => '*', + 'condition_name' => 'package_weight', + 'condition_value' => 0, + 'price' => 10, + 'cost' => 0 + ]; +$connection->query( + "INSERT INTO {$entityTable} (`website_id`, `dest_country_id`, `dest_region_id`, `dest_zip`, `condition_name`," + . "`condition_value`, `price`, `cost`) VALUES (:website_id, :dest_country_id, :dest_region_id, :dest_zip," + . " :condition_name, :condition_value, :price, :cost);", + $data +); diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight_rollback.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight_rollback.php new file mode 100644 index 0000000000000..cb6e9353b8972 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_weight_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'tablerates_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php b/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php new file mode 100644 index 0000000000000..dc2447e8b4c1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php @@ -0,0 +1,69 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; + +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Page Cache state test. + */ +class PageCacheStateTest extends TestCase +{ + /** + * @var PageCacheState + */ + private $pageCacheStateStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->pageCacheStateStorage = $objectManager->get(PageCacheState::class); + } + + /** + * Tests save state. + * + * @param bool $state + * @return void + * @dataProvider saveStateProvider + */ + public function testSave(bool $state): void + { + $this->pageCacheStateStorage->save($state); + $this->assertEquals($state, $this->pageCacheStateStorage->isEnabled()); + } + + /** + * Tests flush state. + * + * @return void + */ + public function testFlush(): void + { + $this->pageCacheStateStorage->save(true); + $this->assertTrue($this->pageCacheStateStorage->isEnabled()); + $this->pageCacheStateStorage->flush(); + $this->assertFalse($this->pageCacheStateStorage->isEnabled()); + } + + /** + * Save state provider. + * + * @return array + */ + public function saveStateProvider(): array + { + return [[true], [false]]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php new file mode 100644 index 0000000000000..17464b6d65861 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Controller\Transparent; + +use Magento\Checkout\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Session\Generic as GenericSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\PaymentMethodManagementInterface; + +/** + * Tests PayPal transparent response controller. + */ +class ResponseTest extends \Magento\TestFramework\TestCase\AbstractController +{ + /** + * Tests setting credit card expiration month and year to payment from PayPal response. + * + * @param string $currentDateTime + * @param string $paypalExpDate + * @param string $expectedCcMonth + * @param string $expectedCcYear + * @throws NoSuchEntityException + * + * @magentoConfigFixture current_store payment/payflowpro/active 1 + * @magentoDataFixture Magento/Sales/_files/quote.php + * @dataProvider paymentCcExpirationDateDataProvider + */ + public function testPaymentCcExpirationDate( + string $currentDateTime, + string $paypalExpDate, + string $expectedCcMonth, + string $expectedCcYear + ) { + $reservedOrderId = 'test01'; + $postData = [ + 'EXPDATE' => $paypalExpDate, + 'AMT' => '0.00', + 'RESPMSG' => 'Verified', + 'CVV2MATCH' => 'Y', + 'PNREF' => 'A10AAD866C87', + 'SECURETOKEN' => '3HYEHfG06skydAdBXbpIl8QJZ', + 'AVSDATA' => 'YNY', + 'RESULT' => '0', + 'IAVS' => 'N', + 'AVSADDR' => 'Y', + 'SECURETOKENID' => 'yqanLisRZbI0HAG8q3SbbKbhiwjNZAGf', + ]; + + $quote = $this->getQuote($reservedOrderId); + $this->getRequest()->setPostValue($postData); + + /** @var Session $checkoutSession */ + $checkoutSession = $this->_objectManager->get(GenericSession::class); + $checkoutSession->setQuoteId($quote->getId()); + $this->setCurrentDateTime($currentDateTime); + + $this->dispatch('paypal/transparent/response'); + + /** @var PaymentMethodManagementInterface $paymentManagment */ + $paymentManagment = $this->_objectManager->get(PaymentMethodManagementInterface::class); + $payment = $paymentManagment->get($quote->getId()); + + $this->assertEquals($expectedCcMonth, $payment->getCcExpMonth()); + $this->assertEquals($expectedCcYear, $payment->getCcExpYear()); + } + + /** + * @return array + */ + public function paymentCcExpirationDateDataProvider(): array + { + return [ + 'Expiration year in current century' => [ + 'currentDateTime' => '2019-07-05 00:00:00', + 'paypalExpDate' => '0321', + 'expectedCcMonth' => 3, + 'expectedCcYear' => 2021 + ], + 'Expiration year in next century' => [ + 'currentDateTime' => '2099-01-01 00:00:00', + 'paypalExpDate' => '1002', + 'expectedCcMonth' => 10, + 'expectedCcYear' => 2102 + ] + ]; + } + + /** + * Sets current date and time. + * + * @param string $date + */ + private function setCurrentDateTime(string $dateTime): void + { + $dateTime = new \DateTime($dateTime, new \DateTimeZone('UTC')); + $dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $dateTimeFactory->method('create') + ->willReturn($dateTime); + + $this->_objectManager->addSharedInstance($dateTimeFactory, DateTimeFactory::class); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return CartInterface + */ + private function getQuote(string $reservedOrderId): CartInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ConverterStub.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ConverterStub.php deleted file mode 100644 index 223ef35c0dcd3..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ConverterStub.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model\Config\Structure\Reader; - -/** - * Class ConverterStub - */ -class ConverterStub extends \Magento\Config\Model\Config\Structure\Converter -{ - /** - * @param \DOMDocument $document - * @return array|null - */ - public function getArrayData(\DOMDocument $document) - { - return $this->_convertDOMDocument($document); - } - - /** - * Convert dom document - * - * @param \DOMNode $source - * @return array - */ - public function convert($source) - { - return $this->_convertDOMDocument($source); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderStub.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderStub.php deleted file mode 100644 index ed1366ad737f9..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderStub.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model\Config\Structure\Reader; - -/** - * Class ReaderStub - */ -class ReaderStub extends \Magento\Config\Model\Config\Structure\Reader -{ - /** - * @param array $fileList - * @return array - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function readFiles(array $fileList) - { - return $this->_readFiles($fileList); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderTest.php deleted file mode 100644 index 6b966a045c982..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderTest.php +++ /dev/null @@ -1,135 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model\Config\Structure\Reader; - -use Magento\Framework\ObjectManagerInterface; - -/** - * Class ReaderTest - */ -class ReaderTest extends \PHPUnit\Framework\TestCase -{ - const EXPECTED = '/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected'; - - const ACTUAL = '/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/actual'; - - /** - * @var ObjectManagerInterface - */ - protected $objectManager; - - /** - * @var \Magento\Framework\App\Utility\Files - */ - protected $fileUtility; - - /** - * @var \Magento\Framework\Config\ValidationStateInterface - */ - protected $validationStateMock; - - /** - * @var \Magento\Framework\Config\SchemaLocatorInterface - */ - protected $schemaLocatorMock; - - /** - * @var \Magento\Framework\Config\FileResolverInterface - */ - protected $fileResolverMock; - - /** - * @var \Magento\Paypal\Model\Config\Structure\Reader\ReaderStub - */ - protected $reader; - - /** - * @var \Magento\Paypal\Model\Config\Structure\Reader\ConverterStub - */ - protected $converter; - - /** - * Set up - * - * @return void - */ - protected function setUp() - { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->fileUtility = \Magento\Framework\App\Utility\Files::init(); - - $this->validationStateMock = $this->getMockBuilder(\Magento\Framework\Config\ValidationStateInterface::class) - ->setMethods(['isValidationRequired']) - ->getMockForAbstractClass(); - $this->schemaLocatorMock = $this->getMockBuilder(\Magento\Config\Model\Config\SchemaLocator::class) - ->disableOriginalConstructor() - ->setMethods(['getPerFileSchema']) - ->getMock(); - $this->fileResolverMock = $this->getMockBuilder(\Magento\Framework\Config\FileResolverInterface::class) - ->getMockForAbstractClass(); - - $this->validationStateMock->expects($this->atLeastOnce()) - ->method('isValidationRequired') - ->willReturn(false); - $this->schemaLocatorMock->expects($this->atLeastOnce()) - ->method('getPerFileSchema') - ->willReturn(false); - - /** @var \Magento\Paypal\Model\Config\Structure\Reader\ConverterStub $converter */ - $this->converter = $this->objectManager->create( - \Magento\Paypal\Model\Config\Structure\Reader\ConverterStub::class - ); - - $this->reader = $this->objectManager->create( - \Magento\Paypal\Model\Config\Structure\Reader\ReaderStub::class, - [ - 'fileResolver' => $this->fileResolverMock, - 'converter' => $this->converter, - 'schemaLocator' => $this->schemaLocatorMock, - 'validationState' => $this->validationStateMock, - 'fileName' => 'no_existing_file.xml', - 'domDocumentClass' => \Magento\Framework\Config\Dom::class - ] - ); - } - - /** - * The test checks the file structure after processing the nodes responsible for inserting content - * - * @return void - */ - public function testXmlConvertedConfigurationAndCompereStructure() - { - $actual = $this->reader->readFiles(['actual' => $this->getActualContent()]); - - $document = new \DOMDocument(); - $document->loadXML($this->getExpectedContent()); - - $expected = $this->converter->getArrayData($document); - - $this->assertEquals($expected, $actual); - } - - /** - * @return string - */ - protected function getActualContent() - { - $files = $this->fileUtility->getFiles([BP . static::ACTUAL], 'config.xml'); - - return file_get_contents(reset($files)); - } - - /** - * @return string - */ - protected function getExpectedContent() - { - $files = $this->fileUtility->getFiles([BP . static::EXPECTED], 'config.xml'); - - return file_get_contents(reset($files)); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml deleted file mode 100644 index 2bd346a6e8f7b..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml +++ /dev/null @@ -1,2369 +0,0 @@ -<?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:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="payment"> - <group id="account" translate="label" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Merchant Location</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="merchant_country" type="select" translate="label comment" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Merchant Country</label> - <comment>If not specified, Default Country from General Config will be used</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Country</frontend_model> - <source_model>Magento\Paypal\Model\System\Config\Source\MerchantCountry</source_model> - <backend_model>Magento\Paypal\Model\System\Config\Backend\MerchantCountry</backend_model> - <config_path>paypal/general/merchant_country</config_path> - </field> - </group> - <group id="recommended_solutions" translate="label" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Recommended Solutions:</label> - <fieldset_css>paypal-top-section paypal-recommended-header</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - </group> - <group id="other_paypal_payment_solutions" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Other PayPal Payment Solutions:</label> - <fieldset_css>paypal-top-section paypal-other-header</fieldset_css> - <frontend_model>\Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> - </group> - <group id="other_payment_methods" translate="label" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Other Payment Methods:</label> - <fieldset_css>paypal-top-section payments-other-header</fieldset_css> - <frontend_model>\Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> - </group> - </section> - <section id="payment_all_paypal" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_payflowpro" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="10"> - <label>Payflow Pro</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <fieldset_css>paypal-other-section</fieldset_css> - <comment><![CDATA[Connect your merchant account with a fully customizable gateway that lets customers pay without leaving your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="activity_path">payment/payflowpro/active</attribute> - <attribute type="paypal_ec_separate">1</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payflow-pro.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="paypal_payflow_api_settings" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payflow Pro</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="partner" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Partner</label> - <config_path>payment/payflowpro/partner</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="user" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>User</label> - <config_path>payment/payflowpro/user</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="vendor" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Vendor</label> - <config_path>payment/payflowpro/vendor</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="pwd" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>payment/payflowpro/pwd</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Test Mode</label> - <config_path>payment/payflowpro/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Use Proxy</label> - <config_path>payment/payflowpro/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>payment/payflowpro/proxy_host</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>payment/payflowpro/proxy_port</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - </group> - <field id="enable_paypal_payflow" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/payflowpro/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="paypal_payflow_api_settings"/> - </requires> - </field> - <field id="payflowpro_cc_vault_active" translate="label" type="select" sortOrder="22" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault Enabled</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <config_path>payment/payflowpro_cc_vault/active</config_path> - <attribute type="shared">1</attribute> - <requires> - <group id="paypal_payflow_api_settings"/> - </requires> - </field> - </group> - <group id="settings_paypal_payflow" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payflow Pro</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> - <config_path>payment/payflowpro/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="payflowpro_cc_vault_title" translate="label" type="text" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault Title</label> - <config_path>payment/payflowpro_cc_vault/title</config_path> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflowpro/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflowpro/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="heading_cc" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Credit Card Settings</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="cctypes" translate="label comment" type="multiselect" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Allowed Credit Card Types</label> - <comment> - <![CDATA[Supporting of American Express cards require additional agreement. Learn more at <a href="http://www.paypal.com/amexupdate">http://www.paypal.com/amexupdate</a>.]]> - </comment> - <config_path>payment/payflowpro/cctypes</config_path> - <source_model>Magento\Paypal\Model\Config::getPayflowproCcTypesAsOptionArray</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settings_paypal_payflow_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflowpro/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflowpro/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflowpro/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflowpro/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="useccv" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Require CVV Entry</label> - <config_path>payment/payflowpro/useccv</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="paypal_payflow_avs_check" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80"> - <label>CVV and AVS Settings</label> - <field id="heading_avs_settings" translate="label" sortOrder="0" showInDefault="1" showInWebsite="1"> - <label>Reject Transaction if:</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_street" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1"> - <label>AVS Street Does Not Match</label> - <config_path>payment/payflowpro/avs_street</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_zip" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1"> - <label>AVS Zip Does Not Match</label> - <config_path>payment/payflowpro/avs_zip</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_international" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1"> - <label>Card Issuer Is Outside The United States</label> - <config_path>payment/payflowpro/avs_international</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_security_code" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1"> - <label>Card Security Code Does Not Match</label> - <config_path>payment/payflowpro/avs_security_code</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">0</attribute> - </field> - </group> - <group id="paypal_payflow_settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="90"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login"/> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password"/> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox"/> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip"/> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path"/> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule"/> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active"/> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule"/> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time"/> - </group> - </group> - </group> - </group> - <group id="payflow_link" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Payflow Link</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <fieldset_css>paypal-other-section</fieldset_css> - <comment><![CDATA[Connect your merchant account with a PCI-compliant gateway that lets customers pay without leaving your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="activity_path">payment/payflow_link/active</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payflow-link.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="payflow_link_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="payflow_link_payflow_link" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payflow Link and Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account" translate="label" sortOrder="5"> - <frontend_class>not-required</frontend_class> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - <attribute type="shared">1</attribute> - </field> - <field id="partner" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Partner</label> - <config_path>payment/payflow_link/partner</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="vendor" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Vendor</label> - <config_path>payment/payflow_link/vendor</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="user" translate="label comment" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>User</label> - <comment>If you do not have multiple users set up on your account, please re-enter your Vendor/Merchant Login here.</comment> - <config_path>payment/payflow_link/user</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="pwd" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>payment/payflow_link/pwd</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Test Mode</label> - <config_path>payment/payflow_link/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Use Proxy</label> - <config_path>payment/payflow_link/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>payment/payflow_link/proxy_host</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>payment/payflow_link/proxy_port</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="payflowlink_info" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink\Info</frontend_model> - <attribute type="shared">1</attribute> - </field> - </group> - - - - <field id="enable_payflow_link" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable Payflow Link</label> - <config_path>payment/payflow_link/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="payflow_link_payflow_link"/> - </requires> - </field> - <field id="enable_express_checkout_basic" translate="label" type="select" sortOrder="40"> - <label>Enable Express Checkout</label> - <config_path>payment/payflow_express/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Express</frontend_model> - </field> - <field id="enable_express_checkout" extends="payment_all_paypal/payflow_link/payflow_link_required/enable_express_checkout_basic" showInDefault="1" showInWebsite="1"> - <requires> - <field id="enable_payflow_link"/> - </requires> - </field> - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="41"> - <comment><![CDATA[Payflow Link lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/payflow_express_bml/active</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Bml</frontend_model> - <requires> - <field id="enable_express_checkout"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> - <config_path>payment/payflow_express_bml/sort_order</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml">1</field> - </depends> - </field> - - - - <group id="payflow_link_advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="60"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="payflow_link_settings_bml_homepage" translate="label" showInWebsite="1" sortOrder="20" showInDefault="1" showInStore="1"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="payflow_link_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="payflow_link_bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_homepage_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_homepage_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_homepage/payflow_link_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="payflow_link_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="payflow_link_settings_bml_categorypage" translate="label" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="payflow_link_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position" /> - <field id="payflow_link_bml_categorypage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="40"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_categorypage_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_categorypage_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_categorypage/payflow_link_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="payflow_link_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="payflow_link_settings_bml_productpage" translate="label" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="50"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="payflow_link_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="payflow_link_bml_productpage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_productpage_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_productpage_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_productpage/payflow_link_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="payflow_link_bml_productpage_position">1</field> - </depends> - </field> - </group> - <group id="payflow_link_settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display" /> - <field id="payflow_link_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position" /> - <field id="payflow_link_bml_checkout_size1" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_checkout_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_checkout_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_checkout/payflow_link_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="settings_payflow_link" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payflow Link</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> - <config_path>payment/payflow_link/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflow_link/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflow_link/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settings_payflow_link_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflow_link/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflow_link/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflow_link/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflow_link/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_editable" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>CVV Entry is Editable</label> - <config_path>payment/payflow_link/csc_editable</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_required" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Require CVV Entry</label> - <config_path>payment/payflow_link/csc_required</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <depends> - <field id="csc_editable">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="email_confirmation" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Send Email Confirmation</label> - <config_path>payment/payflow_link/email_confirmation</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="url_method" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>URL method for Cancel URL and Return URL</label> - <config_path>payment/payflow_link/url_method</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\UrlMethod</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="payflow_link_settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login"/> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password"/> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox"/> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip"/> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path"/> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule"/> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active"/> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule"/> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time"/> - </group> - <group id="payflow_link_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo"/> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages"/> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style"/> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg"/> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor"/> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor"/> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor"/> - </group> - </group> - </group> - <group id="settings_payflow_link_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/title" /> - <field id="sort_order" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/sort_order" /> - <field id="payment_action" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/payment_action" /> - <field id="visible_on_product" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/visible_on_product" /> - <group id="settings_payflow_link_express_checkout_advanced" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/paypal_payflow_express_checkout_advanced"/> - </group> - </group> - <group id="express_checkout" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <fieldset_css>paypal-other-section</fieldset_css> - <comment>Add PayPal as an additional payment method to your checkout page.</comment> - <attribute type="activity_path">payment/paypal_express/active</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-express-checkout.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="express_checkout_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="express_checkout_required_express_checkout" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" translate="label comment tooltip" showInDefault="1" showInWebsite="1" sortOrder="5"> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - <frontend_class>not-required</frontend_class> - <comment> - <![CDATA[<a href="http://www.magentocommerce.com/paypal">Start accepting payments via PayPal!</a>]]> - </comment> - <tooltip>Don't have a PayPal account? Simply enter your email address.</tooltip> - <config_path>paypal/general/business_account</config_path> - <validate>validate-email</validate> - <attribute type="shared">1</attribute> - </field> - <field id="api_authentication" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>API Authentication Methods</label> - <config_path>paypal/wpp/api_authentication</config_path> - <source_model>Magento\Paypal\Model\Config::getApiAuthenticationMethods</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="api_username" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>API Username</label> - <config_path>paypal/wpp/api_username</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="api_password" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>API Password</label> - <config_path>paypal/wpp/api_password</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="api_signature" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>API Signature</label> - <config_path>paypal/wpp/api_signature</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - <depends> - <field id="api_authentication">0</field> - </depends> - </field> - <field id="api_cert" translate="label" type="file" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>API Certificate</label> - <config_path>paypal/wpp/api_cert</config_path> - <backend_model>Magento\Paypal\Model\System\Config\Backend\Cert</backend_model> - <attribute type="shared">1</attribute> - <depends> - <field id="api_authentication">1</field> - </depends> - </field> - <field id="api_wizard" translate="button_label attribute sandbox_button_label" sortOrder="70" showInDefault="1" showInWebsite="1"> - <attribute type="button_label">Get Credentials from PayPal</attribute> - <attribute type="button_url"> - <![CDATA[https://www.paypal.com/webapps/merchantboarding/webflow/externalpartnerflow]]> - </attribute> - - <attribute type="sandbox_button_label">Sandbox Credentials</attribute> - <attribute type="sandbox_button_url"> - <![CDATA[https://www.sandbox.paypal.com/webapps/merchantboarding/webflow/externalpartnerflow]]> - </attribute> - - <!-- partnerId --> - <attribute type="partner_id">NB9WWHYEMVUMS</attribute> - <!-- partnerLogoUrl --> - <attribute type="partner_logo_url">Magento_Backend/web/images/logo-magento.png</attribute> - <!-- receiveCredentials --> - <attribute type="receive_credentials">FALSE</attribute> - <!-- showPermissions --> - <attribute type="show_permissions">FALSE</attribute> - <!-- displayMode --> - <attribute type="display_mode">embedded</attribute> - <!-- productIntentID --> - <attribute type="product_intent_id">pp_express</attribute> - - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\ApiWizard</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Sandbox Mode</label> - <config_path>paypal/wpp/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>API Uses Proxy</label> - <config_path>paypal/wpp/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>paypal/wpp/proxy_host</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="use_proxy">1</field> - </depends> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>paypal/wpp/proxy_port</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="use_proxy">1</field> - </depends> - </field> - </group> - <field id="enable_express_checkout" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/paypal_express/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="express_checkout_required_express_checkout"/> - </requires> - </field> - <field id="enable_in_context_checkout" translate="label comment" type="select" sortOrder="21" showInDefault="1" showInWebsite="1"> - <label>Enable In-Context Checkout Experience</label> - <comment> - <![CDATA[See PayPal Feature Support details and list of supported regions - <a href="https://developer.paypal.com/docs/classic/express-checkout/in-context/" target="_blank">here</a>.]]> - </comment> - <config_path>payment/paypal_express/in_context</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\InContextApi</frontend_model> - <requires> - <field id="enable_express_checkout"/> - </requires> - </field> - <field id="merchant_id" translate="label" type="text" sortOrder="22" showInDefault="1" showInWebsite="1"> - <label>Merchant Account ID</label> - <tooltip>You can look up your merchant ID by logging into https://www.paypal.com/. Click the profile icon on the top right side of the page and then select Profile and settings in the Business Profile menu. (If you do not see the profile icon at the top of the page, click Profile, which appears in the top menu when the My Account tab is selected.) Click My business info on the left, and the Merchant account ID is displayed in the list of profile items on the right.</tooltip> - <config_path>payment/paypal_express/merchant_id</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\MerchantId</frontend_model> - <depends> - <field id="enable_in_context_checkout">1</field> - </depends> - <validate>required-entry</validate> - </field> - <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="1" showInWebsite="1"> - <label>Enable PayPal Credit</label> - <comment><![CDATA[PayPal Express Checkout lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/paypal_express_bml/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\BmlApi</frontend_model> - <requires> - <field id="enable_express_checkout"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" translate="label" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order PayPal Credit</label> - <config_path>payment/paypal_express_bml/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlApiSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml">1</field> - </depends> - </field> - <group id="advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" translate="label comment tooltip" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Publisher ID</label> - <comment><![CDATA[Required to display a banner]]></comment> - <config_path>payment/paypal_express_bml/publisher_id</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="bml_wizard" translate="button_label" sortOrder="15" showInDefault="1" showInWebsite="1"> - <button_label>Get Publisher ID from PayPal</button_label> - <button_url><![CDATA[https://financing.paypal.com/ppfinportal/cart/index?dcp=4eff8563b9cc505e0b9afaff3256705081553c79]]></button_url> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\BmlApiWizard</frontend_model> - </field> - <group id="settings_bml_homepage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/homepage_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_homepage_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/homepage_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsHP</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="bml_homepage_position">0</field> - </depends> - </field> - <field id="bml_homepage_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="settings_bml_categorypage" translate="label" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/categorypage_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_categorypage_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/categorypage_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsCCP</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_categorypage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_categorypage_position">0</field></depends> - </field> - <field id="bml_categorypage_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends><field id="bml_categorypage_position">1</field></depends> - </field> - </group> - <group id="settings_bml_productpage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/productpage_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_productpage_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/productpage_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsCPP</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_productpage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_productpage_position">0</field></depends> - </field> - <field id="bml_productpage_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends><field id="bml_productpage_position">1</field></depends> - </field> - </group> - <group id="settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/checkout_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_checkout_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/checkout_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsCheckout</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_checkout_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_checkout_position">0</field></depends> - </field> - <field id="bml_checkout_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_checkout_position">1</field></depends> - </field> - </group> - </group> - </group> - <group id="settings_ec" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "PayPal" per store views.</comment> - <config_path>payment/paypal_express/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/paypal_express/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/paypal_express/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions\Express</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="visible_on_product" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Product Details Page</label> - <config_path>payment/paypal_express/visible_on_product</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="authorization_honor_period" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Authorization Honor Period (days)</label> - <comment>Specifies what the Authorization Honor Period is on the merchant’s PayPal account. It must mirror the setting in PayPal.</comment> - <config_path>payment/paypal_express/authorization_honor_period</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="payment_action">Order</field> - </depends> - </field> - <field id="order_valid_period" translate="label comment" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Order Valid Period (days)</label> - <comment>Specifies what the Order Valid Period is on the merchant’s PayPal account. It must mirror the setting in PayPal.</comment> - <config_path>payment/paypal_express/order_valid_period</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="payment_action">Order</field> - </depends> - </field> - <field id="child_authorization_number" translate="label comment" type="text" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Number of Child Authorizations</label> - <comment>The default number of child authorizations in your PayPal account is 1. To do multiple authorizations please contact PayPal to request an increase.</comment> - <config_path>payment/paypal_express/child_authorization_number</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="payment_action">Order</field> - </depends> - </field> - <group id="settings_ec_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="80"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="visible_on_cart" translate="label comment" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Shopping Cart</label> - <config_path>payment/paypal_express/visible_on_cart</config_path> - <comment>Also affects mini-shopping cart.</comment> - <source_model>Magento\Paypal\Model\System\Config\Source\Yesnoshortcut</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/paypal_express/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/paypal_express/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="allowspecific">1</field> - </depends> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/paypal_express/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/paypal_express/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="line_items_enabled" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Transfer Cart Line Items</label> - <config_path>payment/paypal_express/line_items_enabled</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="transfer_shipping_options" translate="label tooltip comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Transfer Shipping Options</label> - <config_path>payment/paypal_express/transfer_shipping_options</config_path> - <tooltip>If this option is enabled, customer can change shipping address and shipping method on PayPal website. In live mode works via HTTPS protocol only.</tooltip> - <comment>Notice that PayPal can handle up to 10 shipping options. That is why Magento will transfer only first 10 cheapest shipping options if there are more than 10 available.</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="line_items_enabled">1</field> - </depends> - </field> - <field id="button_flavor" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Shortcut Buttons Flavor</label> - <config_path>paypal/wpp/button_flavor</config_path> - <source_model>Magento\Paypal\Model\Config::getExpressCheckoutButtonFlavors</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="solution_type" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Enable PayPal Guest Checkout</label> - <comment>Ability for buyer to purchase without PayPal account.</comment> - <config_path>payment/paypal_express/solution_type</config_path> - <source_model>Magento\Paypal\Model\Config::getExpressCheckoutSolutionTypes</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="require_billing_address" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Require Customer's Billing Address</label> - <comment>This feature needs be enabled first for the merchant account through PayPal technical support.</comment> - <config_path>payment/paypal_express/require_billing_address</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\RequireBillingAddress</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allow_ba_signup" translate="label comment tooltip" type="select" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Billing Agreement Signup</label> - <comment>Whether to create a billing agreement, if there are no active billing agreements available.</comment> - <tooltip> - <![CDATA[Merchants need to apply to PayPal for enabling billing agreements feature. Do not enable this option until PayPal confirms that billing agreements are enabled for your merchant account.]]> - </tooltip> - <config_path>payment/paypal_express/allow_ba_signup</config_path> - <source_model>Magento\Paypal\Model\Config::getExpressCheckoutBASignupOptions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="skip_order_review_step" translate="label" type="select" sortOrder="95" showInDefault="1" showInWebsite="1"> - <label>Skip Order Review Step</label> - <config_path>payment/paypal_express/skip_order_review_step</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="express_checkout_billing_agreement" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> - <label>PayPal Billing Agreement Settings</label> - <field id="active" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Enabled</label> - <comment> - <![CDATA[Will appear as a payment option only for customers who have at least one active billing agreement.]]> - </comment> - <config_path>payment/paypal_billing_agreement/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <config_path>payment/paypal_billing_agreement/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/paypal_billing_agreement/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/paypal_billing_agreement/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allowspecific" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/paypal_billing_agreement/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/paypal_billing_agreement/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="allowspecific">1</field> - </depends> - </field> - <field id="debug" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/paypal_billing_agreement/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="75" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/paypal_billing_agreement/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="line_items_enabled" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Transfer Cart Line Items</label> - <config_path>payment/paypal_billing_agreement/line_items_enabled</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allow_billing_agreement_wizard" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Allow in Billing Agreement Wizard</label> - <config_path>payment/paypal_billing_agreement/allow_billing_agreement_wizard</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - </group> - <group id="express_checkout_settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="110"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>SFTP Credentials</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_login" translate="label" type="obscure" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Login</label> - <config_path>paypal/fetch_reports/ftp_login</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_password" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>paypal/fetch_reports/ftp_password</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_sandbox" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Sandbox Mode</label> - <config_path>paypal/fetch_reports/ftp_sandbox</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_ip" translate="label comment tooltip" type="text" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Custom Endpoint Hostname or IP-Address</label> - <comment>By default it is "reports.paypal.com".</comment> - <tooltip>Use colon to specify port. For example: "test.example.com:5224".</tooltip> - <config_path>paypal/fetch_reports/ftp_ip</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="settlement_reports_ftp_sandbox">0</field> - </depends> - </field> - <field id="settlement_reports_ftp_path" translate="label comeent" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Custom Path</label> - <comment>By default it is "/ppreports/outgoing".</comment> - <config_path>paypal/fetch_reports/ftp_path</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="settlement_reports_ftp_sandbox">0</field> - </depends> - </field> - <field id="heading_schedule" translate="label" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Scheduled Fetching</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_active" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Enable Automatic Fetching</label> - <config_path>paypal/fetch_reports/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_schedule" translate="label comment" type="select" sortOrder="90" showInDefault="1"> - <label>Schedule</label> - <comment>PayPal retains reports for 45 days.</comment> - <config_path>paypal/fetch_reports/schedule</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\FetchingSchedule</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_time" translate="label" type="time" sortOrder="100" showInDefault="1"> - <label>Time of Day</label> - <config_path>paypal/fetch_reports/time</config_path> - <attribute type="shared">1</attribute> - </field> - </group> - <group id="express_checkout_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="120"> - <label>Frontend Experience Settings</label> - <field id="logo" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Product Logo</label> - <comment>Displays on catalog pages and homepage.</comment> - <config_path>paypal/style/logo</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\Logo</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_pages" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Merchant Pages Style</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="page_style" translate="label tooltip" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Page Style</label> - <config_path>paypal/style/page_style</config_path> - <tooltip> - <![CDATA[Allowable values: "paypal", "primary" (default), your_custom_value (a custom payment page style from your merchant account profile).]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_hdrimg" translate="label tooltip" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Header Image URL</label> - <config_path>paypal/style/paypal_hdrimg</config_path> - <tooltip> - <![CDATA[The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style="color:red">https</strong> is highly encouraged.]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_hdrbackcolor" translate="label tooltip" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Header Background Color</label> - <config_path>paypal/style/paypal_hdrbackcolor</config_path> - <tooltip> - <![CDATA[The background color for the header of the checkout page. Case-insensitive six-character HTML hexadecimal color code in ASCII.]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_hdrbordercolor" translate="label tooltip" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Header Border Color</label> - <config_path>paypal/style/paypal_hdrbordercolor</config_path> - <tooltip>2-pixel perimeter around the header space.</tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_payflowcolor" translate="label tooltip" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Page Background Color</label> - <config_path>paypal/style/paypal_payflowcolor</config_path> - <tooltip> - <![CDATA[The background color for the checkout page around the header and payment form.]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - </group> - </group> - </group> - </group> - <group id="payments_pro_hosted_solution" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Website Payments Pro Hosted Solution</label> - <fieldset_css>paypal-other-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <attribute type="activity_path">payment/hosted_pro/active</attribute> - <comment><![CDATA[Accept payments with a PCI compliant checkout that keeps customers on your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="paypal_ec_separate">1</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-pro.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="pphs_required_settings" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="pphs_required_settings_pphs" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payments Pro Hosted Solution</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account"/> - <field id="api_authentication" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_authentication"/> - <field id="api_username" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_username" /> - <field id="api_password" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_password" /> - <field id="api_signature" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_signature" /> - <field id="api_cert" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_cert" /> - <field id="api_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_wizard" /> - <field id="sandbox_flag" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/sandbox_flag" /> - <field id="use_proxy" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/use_proxy" /> - <field id="proxy_host" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/proxy_host" /> - <field id="proxy_port" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/proxy_port" /> - </group> - <field id="pphs_enable" type="select" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/hosted_pro/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="pphs_required_settings_pphs"/> - </requires> - <frontend_class>paypal-enabler paypal-ec-separate</frontend_class> - </field> - - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="21"> - <comment><![CDATA[Payments Pro Hosted Solution lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <requires> - <field id="pphs_enable"/> - </requires> - </field> - <group id="pphs_advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="22"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="pphs_settings_bml_homepage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="pphs_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="pphs_bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_homepage_position">0</field> - </depends> - </field> - <field id="pphs_bml_homepage_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_homepage/pphs_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="pphs_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="pphs_settings_bml_categorypage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="pphs_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position" /> - <field id="pphs_bml_categorypage_size1" translate="label" showInDefault="1" showInWebsite="1" sortOrder="30" type="select"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_categorypage_position">0</field> - </depends> - </field> - <field id="pphs_bml_categorypage_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_categorypage/pphs_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="pphs_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="pphs_settings_bml_productpage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="pphs_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="pphs_bml_productpage_size1" translate="label" type="select" showInWebsite="1" showInDefault="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_productpage_position">0</field> - </depends> - </field> - <field id="pphs_bml_productpage_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_productpage/pphs_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="pphs_bml_productpage_position">1</field> - </depends> - </field> - </group> - <group id="pphs_settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display" /> - <field id="pphs_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position" /> - <field id="pphs_bml_checkout_size1" translate="label" type="select" showInWebsite="1" showInDefault="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_checkout_position">0</field> - </depends> - </field> - <field id="pphs_bml_checkout_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_checkout/pphs_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="pphs_settings" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payments Pro Hosted Solution</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" type="text" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="10" translate="label comment"> - <label>Title</label> - <comment>It is recommended to set this value to "PayPal" per store views.</comment> - <config_path>payment/hosted_pro/title</config_path> - </field> - <field id="sort_order" type="text" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20" translate="label"> - <label>Sort Order</label> - <config_path>payment/hosted_pro/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - </field> - <field id="payment_action" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Payment Action</label> - <config_path>payment/hosted_pro/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - </field> - <field id="display_ec" type="select" showInDefault="1" showInWebsite="1" sortOrder="40" translate="label"> - <label>Display Express Checkout in the Payment Information step</label> - <config_path>payment/hosted_pro/display_ec</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <group id="pphs_settings_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" type="select" showInDefault="1" showInWebsite="1" sortOrder="10" translate="label"> - <label>Payment Applicable From</label> - <config_path>payment/hosted_pro/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - </field> - <field id="specificcountry" type="multiselect" showInDefault="1" showInWebsite="1" sortOrder="20" translate="label"> - <label>Countries Payment Applicable From</label> - <config_path>payment/hosted_pro/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - </field> - <field id="debug" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Debug Mode</label> - <config_path>payment/hosted_pro/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <field id="verify_peer" type="select" showInDefault="1" showInWebsite="1" sortOrder="35" translate="label"> - <label>Enable SSL verification</label> - <config_path>payment/hosted_pro/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <group id="pphs_settlement_report" showInDefault="1" showInWebsite="1" sortOrder="50" translate="label"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login" /> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password" /> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox" /> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip" /> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path" /> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule" /> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active" /> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule" /> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time" /> - </group> - </group> - </group> - </group> - <group id="payments_pro_hosted_solution_without_bml" extends="payments_pro_hosted_solution"> - <group id="pphs_required_settings"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="pphs_advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </section> - <section id="payment_us" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_group_all_in_one" translate="label comment" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal All-in-One Payment Solutions <i>Accept and process credit cards and PayPal payments.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-all-in-one-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Choose a secure bundled payment solution for your business.</comment> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="payflow_advanced" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Payments Advanced</label> - <fieldset_css>paypal-other-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment><![CDATA[Accept payments with a PCI-compliant checkout that keeps customers on your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="activity_path">payment/payflow_advanced/active</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-advanced.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="required_settings" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="payments_advanced" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payments Advanced and Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account"> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - </field> - <field id="partner" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Partner</label> - <config_path>payment/payflow_advanced/partner</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="vendor" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Vendor</label> - <config_path>payment/payflow_advanced/vendor</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="user" translate="label comment tooltip" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>User</label> - <comment>PayPal recommends that you set up an additional User on your account at manager.paypal.com</comment> - <tooltip>PayPal recommends you set up an additional User on your account at manager.paypal.com, instead of entering your admin username and password here. This will enhance your security and prevent service interruptions if you later change your password. If you do not want to set up an additional User, you can re-enter your Merchant Login here.</tooltip> - <config_path>payment/payflow_advanced/user</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="pwd" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>payment/payflow_advanced/pwd</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Test Mode</label> - <config_path>payment/payflow_advanced/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Use Proxy</label> - <config_path>payment/payflow_advanced/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>payment/payflow_advanced/proxy_host</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>payment/payflow_advanced/proxy_port</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="payflow_advanced_info" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink\Advanced</frontend_model> - <attribute type="shared">1</attribute> - </field> - </group> - <field id="enable_payflow_advanced" translate="label comment" type="select" sortOrder="41" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/payflow_advanced/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="payments_advanced"/> - </requires> - </field> - <field id="enable_express_checkout" extends="payment_all_paypal/payflow_link/payflow_link_required/enable_express_checkout_basic" showInDefault="1" showInWebsite="1"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Hidden</frontend_model> - <requires> - <field id="enable_payflow_advanced"/> - </requires> - </field> - <field id="enable_express_checkout_bml" sortOrder="42" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml"> - <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/payflow_express_bml/active</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Bml</frontend_model> - <requires> - <field id="enable_payflow_advanced"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> - <config_path>payment/payflow_express_bml/sort_order</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml">1</field> - </depends> - </field> - <group id="advanced_advertise_bml" showInDefault="1" showInWebsite="1" sortOrder="60" translate="label comment"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="advanced_settings_bml_homepage" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20" translate="label"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="advanced_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="advanced_bml_homepage_size1" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <depends> - <field id="advanced_bml_homepage_position">0</field> - </depends> - </field> - <field id="advanced_bml_homepage_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_homepage/advanced_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="advanced_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="advanced_settings_bml_categorypage" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30" translate="label"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="advanced_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position"/> - <field id="advanced_bml_categorypage_size1" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_categorypage_position">0</field> - </depends> - </field> - <field id="advanced_bml_categorypage_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_categorypage/advanced_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="advanced_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="advanced_settings_bml_productpage" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40" translate="label"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="advanced_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="advanced_bml_productpage_size1" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" translate="label"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_productpage_position">0</field> - </depends> - </field> - <field id="advanced_bml_productpage_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_productpage/advanced_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="advanced_bml_productpage_position">1</field> - </depends> - </field> - - </group> - <group id="advanced_settings_bml_checkout" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50" translate="label"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display"/> - <field id="advanced_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position"/> - <field id="advanced_bml_checkout_size1" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_checkout_position">0</field> - </depends> - </field> - <field id="advanced_bml_checkout_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_checkout/advanced_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="settings_payments_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payments Advanced</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> - <config_path>payment/payflow_advanced/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflow_advanced/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflow_advanced/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settings_payments_advanced_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflow_advanced/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="25" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflow_advanced/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflow_advanced/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflow_advanced/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_editable" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>CVV Entry is Editable</label> - <config_path>payment/payflow_advanced/csc_editable</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_required" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Require CVV Entry</label> - <config_path>payment/payflow_advanced/csc_required</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <depends> - <field id="csc_editable">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="email_confirmation" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Send Email Confirmation</label> - <config_path>payment/payflow_advanced/email_confirmation</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="url_method" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>URL method for Cancel URL and Return URL</label> - <config_path>payment/payflow_advanced/url_method</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\UrlMethod</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login"/> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password"/> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox"/> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip"/> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path"/> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule"/> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active"/> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule"/> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time"/> - </group> - <group id="frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo"/> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages"/> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style"/> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg"/> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor"/> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor"/> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor"/> - </group> - </group> - </group> - <group id="settings_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/title" /> - <field id="sort_order" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/sort_order" /> - <field id="payment_action" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/payment_action" /> - <field id="visible_on_product" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/visible_on_product" /> - <group id="settings_express_checkout_advanced" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/paypal_payflow_express_checkout_advanced"/> - </group> - </group> - <group id="wpp_usuk" translate="label" sortOrder="40" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout"> - <label>Payments Pro</label> - <attribute type="activity_path">payment/paypal_payment_pro/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-pro.html</comment> - </group> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <field id="enable_paypal_payflow"> - <attribute type="shared">0</attribute> - <config_path>payment/paypal_payment_pro/active</config_path> - </field> - <group id="paypal_payflow_api_settings" translate="label"> - <label>Payments Pro and Express Checkout</label> - </group> - </group> - <group id="settings_paypal_payflow" translate="label"> - <label>Basic Settings - PayPal Payments Pro</label> - </group> - </group> - <group id="wps_express" extends="payment_all_paypal/express_checkout"> - <label>Payments Standard</label> - <comment>Accept credit card and PayPal payments securely.</comment> - <attribute type="activity_path">payment/wps_express/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-standard.html</comment> - </group> - <group id="express_checkout_required"> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <group id="express_checkout_required_express_checkout"> - <label>Payments Standard</label> - </group> - <field id="enable_express_checkout"> - <config_path>payment/wps_express/active</config_path> - </field> - <field id="enable_express_checkout_bml"> - <config_path>payment/wps_express_bml/active</config_path> - </field> - </group> - <group id="settings_ec"> - <label>Basic Settings - PayPal Website Payments Standard</label> - </group> - </group> - </group> - <group id="paypal_payment_gateways" translate="label" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal Payment Gateways <i>Process payments using your own internet merchant account.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-gateways-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="paypal_payflowpro_with_express_checkout" translate="label comment" extends="payment_all_paypal/paypal_payflowpro"> - <label>Payflow Pro</label> - <attribute type="paypal_ec_separate">0</attribute> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <field id="enable_paypal_payflow"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - </field> - <group id="paypal_payflow_api_settings" translate="label"> - <label>Payflow Pro and Express Checkout</label> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account" translate="label" sortOrder="10"> - <frontend_class>not-required</frontend_class> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - <attribute type="shared">1</attribute> - </field> - </group> - <field id="enable_express_checkout" extends="payment_all_paypal/payflow_link/payflow_link_required/enable_express_checkout_basic" showInDefault="1" showInWebsite="1"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Hidden</frontend_model> - <requires> - <field id="enable_paypal_payflow"/> - </requires> - </field> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout_bml_payflow" translate="label" type="select" sortOrder="21" showInWebsite="1" showInDefault="1"> - <label>Enable PayPal Credit</label> - <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/payflow_express_bml/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Bml</frontend_model> - <requires> - <field id="enable_paypal_payflow"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" sortOrder="30" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> - <config_path>payment/payflow_express_bml/sort_order</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml_payflow">1</field> - </depends> - </field> - <group id="paypal_payflow_advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="40"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="paypal_payflow_settings_bml_homepage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="paypal_payflow_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="paypal_payflow_bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_homepage_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_homepage_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_homepage/paypal_payflow_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="paypal_payflow_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="paypal_payflow_settings_bml_categorypage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="paypal_payflow_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position" /> - <field id="paypal_payflow_bml_categorypage_size1" translate="label" sortOrder="30" showInWebsite="1" showInDefault="1" type="select"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_categorypage_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_categorypage_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_categorypage/paypal_payflow_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="paypal_payflow_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="paypal_payflow_settings_bml_productpage" translate="label" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="40"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="paypal_payflow_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="paypal_payflow_bml_productpage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_productpage_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_productpage_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_productpage/paypal_payflow_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="paypal_payflow_bml_productpage_position">1</field> - </depends> - </field> - </group> - <group id="paypal_payflow_settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display" /> - <field id="paypal_payflow_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position" /> - <field id="paypal_payflow_bml_checkout_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_checkout_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_checkout_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_checkout/paypal_payflow_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="settings_paypal_payflow" translate="label"> - <group id="settings_paypal_payflow_advanced" translate="label"> - <group id="paypal_payflow_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo"/> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages"/> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style"/> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg"/> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor"/> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor"/> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor"/> - </group> - </group> - </group> - <group id="paypal_payflow_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <config_path>payment/payflow_express/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflow_express/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflow_express/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="visible_on_product" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Product Details Page</label> - <config_path>payment/payflow_express/visible_on_product</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="paypal_payflow_express_checkout_advanced" translate="label" showInDefault="1" showInWebsite="1" sortOrder="60"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="visible_on_cart" translate="label comment" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Shopping Cart</label> - <comment>Also affects mini-shopping cart.</comment> - <config_path>payment/payflow_express/visible_on_cart</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\Yesnoshortcut</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflow_express/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflow_express/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflow_express/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflow_express/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="line_items_enabled" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Transfer Cart Line Items</label> - <config_path>payment/payflow_express/line_items_enabled</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="skip_order_review_step" sortOrder="50" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/skip_order_review_step"/> - </group> - </group> - </group> - <group id="payflow_link_us" extends="payment_all_paypal/payflow_link"/> - </group> - <group id="paypal_alternative_payment_methods" sortOrder="5" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="express_checkout_us" translate="label comment" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content</help_url> - <attribute type="shared">0</attribute> - <attribute type="activity_path">payment/paypal_express/active</attribute> - <attribute type="activity_path">payment/payflow_express/active</attribute> - <attribute type="displayIn">recommended_solutions</attribute> - </group> - </group> - </section> - <section id="payment_gb" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_group_all_in_one" translate="label comment" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal All-in-One Payment Solutions  <i>Accept and process credit cards and PayPal payments.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-all-in-one-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Choose a secure bundled payment solution for your business.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="wpp_usuk" translate="comment" sortOrder="20"> - <fieldset_css>pp-general-uk</fieldset_css> - <demo_link>http://www.youtube.com/watch?v=LBe-TW87eGI&list=PLF18B1094ABCD7CE8&index=1&feature=plpp_video</demo_link> - <comment>Accept payments with a completely customizable checkout page.</comment> - <group id="wpp_required_settings"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="wpp_advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - <group id="payments_pro_hosted_solution_with_express_checkout" translate="label comment" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="30"> - <label>Website Payments Pro Hosted Solution</label> - <attribute type="paypal_ec_separate">0</attribute> - <group id="pphs_required_settings"> - <group id="pphs_required_settings_pphs" translate="label"> - <label>Website Payments Pro Hosted Solution and Express Checkout</label> - </group> - <field id="pphs_enable"> - <requires> - <group id="pphs_required_settings_pphs"/> - </requires> - <frontend_class>paypal-enabler</frontend_class> - </field> - </group> - <group id="pphs_settings" translate="label"> - <label>Basic Settings - PayPal Website Payments Pro Hosted Solution</label> - <group id="pphs_settings_advanced"> - <group id="pphs_billing_agreement" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>PayPal Billing Agreement Settings</label> - <field id="active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/active" /> - <field id="title" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/title" /> - <field id="sort_order" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/sort_order" /> - <field id="payment_action" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/payment_action" /> - <field id="allowspecific" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/allowspecific" /> - <field id="specificcountry" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/specificcountry" /> - <field id="debug" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/debug" /> - <field id="verify_peer" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/verify_peer" /> - <field id="line_items_enabled" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/line_items_enabled" /> - <field id="allow_billing_agreement_wizard" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/allow_billing_agreement_wizard" /> - </group> - <group id="pphs_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo" /> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages" /> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style" /> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg" /> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor" /> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor" /> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor" /> - </group> - </group> - </group> - <group id="pphs_settings_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" extends="payment_all_paypal/express_checkout/settings_ec/title" /> - <field id="sort_order" extends="payment_all_paypal/express_checkout/settings_ec/sort_order" /> - <field id="payment_action" extends="payment_all_paypal/express_checkout/settings_ec/payment_action" /> - <field id="visible_on_product" extends="payment_all_paypal/express_checkout/settings_ec/visible_on_product" /> - <field id="authorization_honor_period" extends="payment_all_paypal/express_checkout/settings_ec/authorization_honor_period" /> - <field id="order_valid_period" extends="payment_all_paypal/express_checkout/settings_ec/order_valid_period" /> - <field id="child_authorization_number" extends="payment_all_paypal/express_checkout/settings_ec/child_authorization_number" /> - <group id="pphs_settings_express_checkout_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="80"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="visible_on_cart" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/visible_on_cart" /> - <field id="allowspecific" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/allowspecific" /> - <field id="specificcountry" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/specificcountry" /> - <field id="debug" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/debug" /> - <field id="verify_peer" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/verify_peer" /> - <field id="line_items_enabled" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/line_items_enabled" /> - <field id="transfer_shipping_options" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/transfer_shipping_options" /> - <field id="button_flavor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/button_flavor" /> - <field id="solution_type" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/solution_type" /> - <field id="require_billing_address" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/require_billing_address" /> - <field id="allow_ba_signup" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/allow_ba_signup" /> - <field id="skip_order_review_step" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/skip_order_review_step" /> - </group> - </group> - </group> - <group id="wps_express" extends="payment_all_paypal/express_checkout" sortOrder="50"> - <label>Website Payments Standard</label> - <comment>Accept credit card and PayPal payments securely.</comment> - <attribute type="activity_path">payment/wps_express/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-standard.html</comment> - </group> - <group id="express_checkout_required"> - <group id="express_checkout_required_express_checkout"> - <label>Website Payments Standard</label> - </group> - <field id="enable_express_checkout"> - <config_path>payment/wps_express/active</config_path> - </field> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - <group id="settings_ec"> - <label>Basic Settings - PayPal Website Payments Standard</label> - </group> - </group> - </group> - <group id="paypal_alternative_payment_methods" sortOrder="5" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="express_checkout_gb" translate="label comment" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content</help_url> - <attribute type="displayIn">recommended_solutions</attribute> - <group id="express_checkout_required"> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </group> - </section> - <section id="payment_de" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_payment_solutions" showInDefault="0" showInWebsite="0" showInStore="0" sortOrder="5"> - <group id="express_checkout_de" translate="label comment" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">recommended_solutions</attribute> - <group id="express_checkout_required"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - <group id="settings_ec"> - <group id="settings_ec_advanced"> - <field id="solution_type" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </group> - </group> - </section> - <section id="payment_other" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="express_checkout_other" translate="label comment" sortOrder="5" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">recommended_solutions</attribute> - <group id="express_checkout_required"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - <group id="paypal_group_all_in_one" translate="label comment" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal All-in-One Payment Solutions  <i>Accept and process credit cards and PayPal payments.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-all-in-one-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Choose a secure bundled payment solution for your business.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="wps_other" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Website Payments Standard</label> - <fieldset_css>complex</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Accept credit card and PayPal payments securely.</comment> - <attribute type="activity_path">payment/wps_express/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-standard.html</comment> - </group> - <group id="express_checkout_required"> - <group id="express_checkout_required_express_checkout"> - <label>Website Payments Standard</label> - </group> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout"> - <config_path>payment/wps_express/active</config_path> - </field> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - <group id="settings_ec"> - <label>Basic Settings - PayPal Website Payments Standard</label> - </group> - </group> - </group> - <group id="paypal_payment_gateways" translate="label comment" sortOrder="8" showInDefault="0" showInWebsite="0" showInStore="0"> - <label><![CDATA[PayPal Payment Gateways <i>Process payments using your own internet merchant account.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-gateways-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Process payments using your own internet merchant account.</comment> - <help_url>https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content</help_url> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - </group> - </section> - <section id="payment_ca" extends="payment_other"> - <group id="express_checkout_other"> - <attribute type="activity_path">payment/paypal_express/active</attribute> - <attribute type="activity_path">payment/payflow_express/active</attribute> - </group> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="20"/> - </group> - <group id="paypal_payment_gateways" showInDefault="1" showInWebsite="1" showInStore="1"> - <fieldset_css>complex paypal-other-section paypal-gateways-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <label><![CDATA[PayPal Payment Gateways <i>Process payments using your own internet merchant account.</i>]]></label> - <group id="wpp_ca" translate="label" extends="payment_all_paypal/paypal_payflowpro" sortOrder="30"> - <label>Website Payments Pro</label> - <attribute type="activity_path">payment/paypal_payment_pro/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-pro.html</comment> - </group> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <group id="paypal_payflow_api_settings"> - <label>Payments Pro</label> - </group> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="enable_paypal_payflow"> - <frontend_class>paypal-enabler paypal-ec-pe</frontend_class> - <attribute type="shared">0</attribute> - <config_path>payment/paypal_payment_pro/active</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - </field> - </group> - <group id="settings_paypal_payflow"> - <label>Basic Settings - PayPal Payments Pro</label> - </group> - </group> - <group id="paypal_payflowpro_ca" extends="payment_all_paypal/paypal_payflowpro" sortOrder="40"/> - <group id="payflow_link_ca" extends="payment_all_paypal/payflow_link" sortOrder="50"> - <group id="payflow_link_required"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="payflow_link_advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </group> - </section> - <section id="payment_au" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_au" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"/> - </group> - <group id="paypal_payment_gateways" showInDefault="1" showInWebsite="1" showInStore="1"> - <group id="paypal_payflowpro_au" extends="payment_all_paypal/paypal_payflowpro" sortOrder="20"/> - </group> - </section> - <section id="payment_jp" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_jp" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Website Payments Plus</label> - </group> - </group> - </section> - <section id="payment_fr" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_fr" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Integral Evolution</label> - </group> - </group> - </section> - <section id="payment_it" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_it" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Pro</label> - </group> - </group> - </section> - <section id="payment_es" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_es" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Pasarela integral</label> - </group> - </group> - </section> - <section id="payment_hk" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_hk" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"/> - </group> - </section> - <section id="payment_nz" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other"/> - </group> - <group id="paypal_payment_gateways" showInDefault="1" showInWebsite="1" showInStore="1"> - <group id="paypal_payflowpro_nz" extends="payment_all_paypal/paypal_payflowpro"/> - </group> - </section> - </system> -</config> diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index cb83fa3abd857..3f7f8719fd587 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -72,7 +72,7 @@ protected function setUp() $this->api = $this->getMockBuilder(Nvp::class) ->disableOriginalConstructor() - ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress']) + ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress', 'getShippingRateCode']) ->getMock(); $this->api->expects($this->any()) @@ -302,6 +302,8 @@ public function testReturnFromPaypal() public function testReturnFromPaypalButton() { $quote = $this->getFixtureQuote(); + $quote->getShippingAddress()->setShippingMethod(''); + $this->prepareCheckoutModel($quote); $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); @@ -317,6 +319,8 @@ public function testReturnFromPaypalButton() $this->assertEquals($exportedShippingData['telephone'], $shippingAddress->getTelephone()); $this->assertEquals($exportedShippingData['email'], $shippingAddress->getEmail()); + $this->assertEquals('flatrate_flatrate', $shippingAddress->getShippingMethod()); + $this->assertEquals([$exportedShippingData['street']], $billingAddress->getStreet()); $this->assertEquals($exportedShippingData['firstname'], $billingAddress->getFirstname()); $this->assertEquals($exportedShippingData['city'], $billingAddress->getCity()); @@ -551,6 +555,9 @@ private function prepareCheckoutModel(Quote $quote, $prefix = '') $this->api->method('getExportedShippingAddress') ->willReturn($exportedShippingAddress); + $this->api->method('getShippingRateCode') + ->willReturn('flatrate_flatrate Flat Rate - Fixed'); + $this->paypalInfo->method('importToPayment') ->with($this->api, $quote->getPayment()); } @@ -573,7 +580,7 @@ private function getExportedData(): array 'city' => 'Denver', 'street' => '66 Pearl St', 'postcode' => '80203', - 'telephone' => '555-555-555' + 'telephone' => '555-555-555', ], 'billing' => [ 'email' => 'customer@example.com', diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 72c5d7736a30d..15f555a67e722 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -98,6 +98,9 @@ public function testSetCustomerData(): void $customer = $quote->getCustomer(); $this->assertEquals($expected, $this->convertToArray($customer)); $this->assertEquals('qa@example.com', $quote->getCustomerEmail()); + $this->assertEquals('Joe', $quote->getCustomerFirstname()); + $this->assertEquals('Dou', $quote->getCustomerLastname()); + $this->assertEquals('Ivan', $quote->getCustomerMiddlename()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php new file mode 100644 index 0000000000000..9a611b8f2b9ea --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Rss\Controller\Feed; + +class IndexTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * @var \Magento\Rss\Model\UrlBuilder + */ + private $urlBuilder; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var \Magento\Wishlist\Model\Wishlist + */ + private $wishlist; + + /** + * @var + */ + private $customerSession; + + protected function setUp() + { + parent::setUp(); + $this->urlBuilder = $this->_objectManager->get(\Magento\Rss\Model\UrlBuilder::class); + $this->customerRepository = $this->_objectManager->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->wishlist = $this->_objectManager->get(\Magento\Wishlist\Model\Wishlist::class); + $this->customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + } + + /** + * Check Rss response. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + */ + public function testRssResponse() + { + $firstCustomerId = 1; + $this->customerSession->setCustomerId($firstCustomerId); + $customer = $this->customerRepository->getById($firstCustomerId); + $customerEmail = $customer->getEmail(); + $wishlistId = $this->wishlist->loadByCustomerId($firstCustomerId)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + $this->assertContains('<title>John Smith\'s Wishlist', $body); + } + + /** + * Check Rss with incorrect wishlist id. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + */ + public function testRssResponseWithIncorrectWishlistId() + { + $firstCustomerId = 1; + $secondCustomerId = 2; + $this->customerSession->setCustomerId($firstCustomerId); + $customer = $this->customerRepository->getById($firstCustomerId); + $customerEmail = $customer->getEmail(); + $wishlistId = $this->wishlist->loadByCustomerId($secondCustomerId, true)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + $this->assertContains('404 Not Found', $body); + } + + private function getLink($customerId, $customerEmail, $wishlistId) + { + + return 'rss/feed/index/type/wishlist/data/' + . base64_encode($customerId . ',' . $customerEmail) + . '/wishlist_id/' . $wishlistId; + } +} 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 999522a49e006..b75501911be6b 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -5,104 +5,166 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterfaceFactory; +use Magento\Customer\Model\Data\Option; use Magento\Customer\Model\Metadata\Form; use Magento\Customer\Model\Metadata\FormFactory; use Magento\Framework\View\LayoutInterface; use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; /** + * Class for test Account + * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountTest extends \PHPUnit\Framework\TestCase { - /** @var Account */ + /** + * @var Account + */ private $accountBlock; /** - * @var Bootstrap + * @var ObjectManager */ private $objectManager; /** - * @magentoDataFixture Magento/Sales/_files/quote.php + * @var SessionQuote|MockObject + */ + private $session; + + /** + * @inheritdoc */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $quote = $this->objectManager->create(Quote::class)->load(1); - $sessionQuoteMock = $this->getMockBuilder( - SessionQuote::class - )->disableOriginalConstructor()->setMethods( - ['getCustomerId', 'getStore', 'getStoreId', 'getQuote'] - )->getMock(); - $sessionQuoteMock->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); - $sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quote)); - /** @var LayoutInterface $layout */ - $layout = $this->objectManager->get(LayoutInterface::class); - $this->accountBlock = $layout->createBlock( - Account::class, - 'address_block' . rand(), - ['sessionQuote' => $sessionQuoteMock] - ); parent::setUp(); } /** + * Test for get form with existing customer + * * @magentoDataFixture Magento/Customer/_files/customer.php */ - public function testGetForm() + public function testGetFormWithCustomer() { + $customerGroup = 2; + $quote = $this->objectManager->create(Quote::class); + + $this->session = $this->getMockBuilder(SessionQuote::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId','getQuote']) + ->getMock(); + $this->session->method('getQuote') + ->willReturn($quote); + $this->session->method('getCustomerId') + ->willReturn(1); + + /** @var LayoutInterface $layout */ + $layout = $this->objectManager->get(LayoutInterface::class); + $this->accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand(), + ['sessionQuote' => $this->session] + ); + + $fixtureCustomerId = 1; + /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $customerRepository->getById($fixtureCustomerId); + $customer->setGroupId($customerGroup); + $customerRepository->save($customer); + $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); - $this->assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); + self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; + $content = $form->toHtml(); - $this->assertEquals(count($expectedFields), $fieldset->getElements()->count()); + self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); foreach ($fieldset->getElements() as $element) { - $this->assertTrue( + self::assertTrue( in_array($element->getId(), $expectedFields), sprintf('Unexpected field "%s" in form.', $element->getId()) ); } + + self::assertContains( + '', + $content, + 'The Customer Group specified for the chosen customer should be selected.' + ); + + self::assertContains( + 'value="'.$customer->getEmail().'"', + $content, + 'The Customer Email specified for the chosen customer should be input ' + ); } /** * Tests a case when user defined custom attribute has default value. * - * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoConfigFixture current_store customer/create_account/default_group 2 + * @magentoConfigFixture secondstore_store customer/create_account/default_group 3 */ public function testGetFormWithUserDefinedAttribute() { + /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ + $storeManager = Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class); + $secondStore = $storeManager->getStore('secondstore'); + + $quoteSession = $this->objectManager->get(SessionQuote::class); + $quoteSession->setStoreId($secondStore->getId()); + $formFactory = $this->getFormFactoryMock(); $this->objectManager->addSharedInstance($formFactory, FormFactory::class); /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); - $accountBlock = $layout->createBlock(Account::class, 'address_block' . rand()); + $accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand() + ); $form = $accountBlock->getForm(); $form->setUseContainer(true); + $content = $form->toHtml(); - $this->assertContains( + self::assertContains( '', - $form->toHtml(), - 'Default value for user defined custom attribute should be selected' + $content, + 'Default value for user defined custom attribute should be selected.' + ); + + self::assertContains( + '', + $content, + 'The Customer Group specified for the chosen store should be selected.' ); } /** - * @return \PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Form object. + * + * @return MockObject */ - private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject + private function getFormFactoryMock(): MockObject { /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); @@ -113,11 +175,12 @@ private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject ->setDefaultValue('1') ->setFrontendLabel('Yes/No'); + /** @var Form|MockObject $form */ $form = $this->getMockBuilder(Form::class) ->disableOriginalConstructor() ->getMock(); $form->method('getUserAttributes')->willReturn([$booleanAttribute]); - $form->method('getSystemAttributes')->willReturn([]); + $form->method('getSystemAttributes')->willReturn([$this->createCustomerGroupAttribute()]); $formFactory = $this->getMockBuilder(FormFactory::class) ->disableOriginalConstructor() @@ -126,4 +189,33 @@ private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject return $formFactory; } + + /** + * Creates a customer group attribute object. + * + * @return AttributeMetadataInterface + */ + private function createCustomerGroupAttribute(): AttributeMetadataInterface + { + /** @var Option $option1 */ + $option1 = $this->objectManager->create(Option::class); + $option1->setValue(2); + $option1->setLabel('Wholesale'); + + /** @var Option $option2 */ + $option2 = $this->objectManager->create(Option::class); + $option2->setValue(3); + $option2->setLabel('Retailer'); + + /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ + $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); + $attribute = $attributeMetadataFactory->create() + ->setAttributeCode('group_id') + ->setBackendType('static') + ->setFrontendInput('select') + ->setOptions([$option1, $option2]) + ->setIsRequired(true); + + return $attribute; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php new file mode 100644 index 0000000000000..1125fc1730718 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php @@ -0,0 +1,59 @@ +layout = $this->_objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Totals::class, 'totals_block'); + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_free_shipping_by_coupon.php + */ + public function testShowShippingCoupon() + { + /** @var Order $order */ + $order = $this->orderFactory->create(); + $order->loadByIncrementId('100000001'); + + $this->block->setOrder($order); + $this->block->toHtml(); + + $shippingTotal = $this->block->getTotal('shipping'); + $this->assertNotFalse($shippingTotal, 'Shipping method is absent on the total\'s block.'); + $this->assertContains( + '1234567890', + $shippingTotal->getLabel(), + 'Coupon code is absent in the shipping method label name.' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php index e2638b5df1f88..f863edd049258 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php @@ -9,21 +9,61 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Request\Http; +use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\MessageInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\Service\OrderService; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\TestCase\AbstractBackendController; +use PHPUnit\Framework\Constraint\StringContains; use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Class test backend order save. + * + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends AbstractBackendController { + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var FormKey + */ + private $formKey; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::create'; + + /** + * @var string + */ + protected $uri = 'backend/sales/order_create/save'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + /** * Checks a case when order creation is failed on payment method processing but new customer already created * in the database and after new controller dispatching the customer should be already loaded in session * to prevent invalid validation. * - * @magentoAppArea adminhtml * @magentoDataFixture Magento/Sales/_files/quote_with_new_customer.php */ public function testExecuteWithPaymentOperation() @@ -36,7 +76,7 @@ public function testExecuteWithPaymentOperation() $email = 'john.doe001@test.com'; $data = [ 'account' => [ - 'email' => $email + 'email' => $email, ] ]; $this->getRequest()->setMethod(Http::METHOD_POST); @@ -66,13 +106,52 @@ public function testExecuteWithPaymentOperation() $this->_objectManager->removeSharedInstance(OrderService::class); } + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * + * @return void + */ + public function testSendEmailOnOrderSave(): void + { + $this->prepareRequest(['send_confirmation' => true]); + $this->dispatch('backend/sales/order_create/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You created the order.')]), + MessageInterface::TYPE_SUCCESS + ); + + $this->assertRedirect($this->stringContains('sales/order/view/')); + + $orderId = $this->getOrderId(); + if ($orderId === false) { + $this->fail('Order is not created.'); + } + $order = $this->getOrder($orderId); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } + /** * Gets quote by reserved order id. * * @param string $reservedOrderId * @return \Magento\Quote\Api\Data\CartInterface */ - private function getQuote($reservedOrderId) + private function getQuote(string $reservedOrderId): \Magento\Quote\Api\Data\CartInterface { /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); @@ -82,6 +161,81 @@ private function getQuote($reservedOrderId) /** @var CartRepositoryInterface $quoteRepository */ $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); $items = $quoteRepository->getList($searchCriteria)->getItems(); + return array_pop($items); } + + /** + * @inheritdoc + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param int $orderId + * @return OrderInterface + */ + private function getOrder(int $orderId): OrderInterface + { + return $this->_objectManager->get(OrderRepository::class)->get($orderId); + } + + /** + * @param array $params + * @return void + */ + private function prepareRequest(array $params = []): void + { + $quote = $this->getQuote('guest_quote'); + $session = $this->_objectManager->get(Quote::class); + $session->setQuoteId($quote->getId()); + $session->setCustomerId(0); + + $email = 'john.doe001@test.com'; + $data = [ + 'account' => [ + 'email' => $email, + ], + ]; + + $data = array_replace_recursive($data, $params); + + $this->getRequest() + ->setMethod('POST') + ->setParams(['form_key' => $this->formKey->getFormKey()]) + ->setPostValue(['order' => $data]); + } + + /** + * @return string|bool + */ + protected function getOrderId() + { + $currentUrl = $this->getResponse()->getHeader('Location'); + $orderId = false; + + if (preg_match('/order_id\/(?\d+)/', $currentUrl, $matches)) { + $orderId = $matches['order_id'] ?? ''; + } + + return $orderId; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php new file mode 100644 index 0000000000000..2a7731715021b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php @@ -0,0 +1,92 @@ +transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return CreditmemoInterface + */ + protected function getCreditMemo(OrderInterface $order): CreditmemoInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection $creditMemoCollection */ + $creditMemoCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Creditmemo\CollectionFactory::class + )->create(); + + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $creditMemoCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $creditMemo; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php new file mode 100644 index 0000000000000..2f23da8b3db87 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php @@ -0,0 +1,102 @@ +prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/sales/order_creditmemo/addComment'); + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject =__('Update to your %1 credit memo', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $creditmemo = $this->getCreditMemo($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $creditmemo->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php new file mode 100644 index 0000000000000..f589a0f5a1c74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -0,0 +1,189 @@ +prepareRequest(['creditmemo' => ['send_email' => true]]); + $this->dispatch('backend/sales/order_creditmemo/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You created the credit memo.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $creditMemo = $this->getCreditMemo($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Credit memo for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $creditMemo->getStore()->getFrontendName() + ), + new StringContains( + "Your Credit Memo #{$creditMemo->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * Test order will keep same(custom) status after partial refund, if state has not been changed. + * + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusPartialRefund() + { + /** @var Order $existingOrder */ + $existingOrder = $this->_objectManager->create(Order::class)->loadByIncrementId('100000001'); + $items = $this->getOrderItems($existingOrder, 1); + $requestParams = [ + 'creditmemo' => [ + 'items' => $items, + 'do_offline' => '1', + 'comment_text' => '', + 'shipping_amount' => '0', + 'adjustment_positive' => '0', + 'adjustment_negative' => '0', + ], + 'order_id' => $existingOrder->getId(), + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($requestParams); + $this->dispatch('backend/sales/order_creditmemo/save'); + + /** @var Order $updatedOrder */ + $updatedOrder = $this->_objectManager->create(Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('custom_processing', $updatedOrder->getStatus()); + $this->assertSame('processing', $updatedOrder->getState()); + } + + /** + * Test order will change custom status after total refund, when state has been changed. + * + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusTotalRefund() + { + /** @var Order $existingOrder */ + $existingOrder = $this->_objectManager->create(Order::class)->loadByIncrementId('100000001'); + $requestParams = [ + 'creditmemo' => [ + 'items' => $this->getOrderItems($existingOrder), + 'do_offline' => '1', + 'comment_text' => '', + 'shipping_amount' => '0', + 'adjustment_positive' => '0', + 'adjustment_negative' => '0', + ], + 'order_id' => $existingOrder->getId(), + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($requestParams); + $this->dispatch('backend/sales/order_creditmemo/save'); + + /** @var Order $updatedOrder */ + $updatedOrder = $this->_objectManager->create(Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('complete', $updatedOrder->getStatus()); + $this->assertSame('complete', $updatedOrder->getState()); + } + + /** + * Gets all items of given Order in proper format. + * + * @param Order $order + * @param int $subQty + * @return array + */ + private function getOrderItems(Order $order, int $subQty = 0) + { + $items = []; + /** @var OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + $items[$item->getItemId()] = [ + 'qty' => $item->getQtyOrdered() - $subQty, + ]; + } + + return $items; + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = ['creditmemo' => ['do_offline' => true]]; + $data = array_replace_recursive($data, $params); + + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php new file mode 100644 index 0000000000000..4d19106ad8e51 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php @@ -0,0 +1,136 @@ +orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + } + + /** + * @return void + */ + public function testSendOrderEmail(): void + { + $order = $this->prepareRequest(); + $this->dispatch('backend/sales/order/email'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You sent the order email.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + + $redirectUrl = 'sales/order/view/order_id/' . $order->getEntityId(); + $this->assertRedirect($this->stringContains($redirectUrl)); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + private function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @return OrderInterface|null + */ + private function prepareRequest() + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setParams(['order_id' => $order->getEntityId()]); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php new file mode 100644 index 0000000000000..3ba54418b6c26 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php @@ -0,0 +1,92 @@ +transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return InvoiceInterface + */ + protected function getInvoiceByOrder(OrderInterface $order): InvoiceInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection $invoiceCollection */ + $invoiceCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory::class + )->create(); + + /** @var InvoiceInterface $invoice */ + $invoice = $invoiceCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $invoice; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php new file mode 100644 index 0000000000000..81e1dd7afc496 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -0,0 +1,103 @@ +prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/sales/order_invoice/addComment'); + + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $invoice->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php new file mode 100644 index 0000000000000..85223528ec82a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php @@ -0,0 +1,88 @@ +getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + + $this->getRequest()->setParams(['invoice_id' => $invoice->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/email'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You sent the message.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + + $redirectUrl = sprintf( + 'sales/invoice/view/order_id/%s/invoice_id/%s', + $order->getEntityId(), + $invoice->getEntityId() + ); + $this->assertRedirect($this->stringContains($redirectUrl)); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($invoice->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $invoice->getStore()->getFrontendName() + ), + new StringContains( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->uri .= '/invoice_id/' . $invoice->getEntityId(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->uri .= '/invoice_id/' . $invoice->getEntityId(); + + parent::testAclNoAccess(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php new file mode 100644 index 0000000000000..68074e38d9a39 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php @@ -0,0 +1,97 @@ +prepareRequest(['invoice' => ['send_email' => true]]); + $this->dispatch('backend/sales/order_invoice/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('The invoice has been created.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $invoice = $this->getInvoiceByOrder($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($invoice->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $invoice->getStore()->getFrontendName() + ), + new StringContains( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php new file mode 100644 index 0000000000000..1067a474e19aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php @@ -0,0 +1,109 @@ +prepareRequestData(); + $this->dispatch('sales/guest/view/'); + $content = $this->getResponse()->getBody(); + $this->assertContains('Order # 100000001', $content); + } + + /** + * View order as logged in customer + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testViewOrderAsLoggedIn() + { + $this->login(1); + $this->dispatch('sales/guest/view/'); + $this->assertRedirect($this->stringContains('sales/order/history/')); + } + + /** + * Test attempting to open the Returns form as logged in customer + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testAttemptToOpenTheFormAsLoggedIn() + { + $this->login(1); + $this->dispatch('sales/guest/form/'); + $this->assertRedirect($this->stringContains('customer/account')); + } + + /** + * Test Return Order for guest with incorrect data + */ + public function testViewOrderAsGuestWithIncorrectData() + { + $this->prepareRequestData(true); + $this->dispatch('sales/guest/view/'); + $this->assertSessionMessages( + $this->equalTo(['You entered incorrect data. Please try again.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $session->loginById($customerId); + } + + /** + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + $orderId = 100000001; + $email = $invalidData ? 'wrong@example.com' : 'customer@null.com'; + + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'oar_order_id' => $orderId, + 'oar_billing_lastname' => 'lastname', + 'oar_type' => 'email', + 'oar_email' => $email, + 'oar_zip' => '', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php new file mode 100644 index 0000000000000..1035ce1592314 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php @@ -0,0 +1,100 @@ +objectManager = Bootstrap::getObjectManager(); + $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class); + $this->formKey = $this->objectManager->get(FormKey::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * @return void + */ + public function testSendEmailOnOrderPlace(): void + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('guest_quote', 'reserved_order_id'); + + $checkoutSession = $this->objectManager->get(CheckoutSession::class); + $checkoutSession->setQuoteId($quote->getId()); + + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->load($quote->getId(), 'quote_id'); + $cartId = $quoteIdMask->getMaskedId(); + + /** @var GuestCartManagementInterface $cartManagement */ + $cartManagement = $this->objectManager->get(GuestCartManagementInterface::class); + $orderId = $cartManagement->placeOrder($cartId); + $order = $this->objectManager->get(OrderRepository::class)->get($orderId); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Service/InvoiceServiceTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Service/InvoiceServiceTest.php new file mode 100644 index 0000000000000..e35c480e44c66 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Service/InvoiceServiceTest.php @@ -0,0 +1,92 @@ +invoiceService = Bootstrap::getObjectManager()->create(InvoiceService::class); + } + + /** + * @param int $invoiceQty + * @magentoDataFixture Magento/Sales/_files/order_configurable_product.php + * @return void + * @dataProvider prepareInvoiceConfigurableProductDataProvider + */ + public function testPrepareInvoiceConfigurableProduct(int $invoiceQty): void + { + /** @var OrderInterface $order */ + $order = Bootstrap::getObjectManager()->create(Order::class)->load('100000001', 'increment_id'); + $orderItems = $order->getItems(); + foreach ($orderItems as $orderItem) { + if ($orderItem->getParentItemId()) { + $parentItemId = $orderItem->getParentItemId(); + } + } + $invoice = $this->invoiceService->prepareInvoice($order, [$parentItemId => $invoiceQty]); + $invoiceItems = $invoice->getItems(); + foreach ($invoiceItems as $invoiceItem) { + $this->assertEquals($invoiceQty, $invoiceItem->getQty()); + } + } + + public function prepareInvoiceConfigurableProductDataProvider() + { + return [ + 'full invoice' => [2], + 'partial invoice' => [1] + ]; + } + + /** + * @param int $invoiceQty + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + * @dataProvider prepareInvoiceSimpleProductDataProvider + */ + public function testPrepareInvoiceSimpleProduct(int $invoiceQty): void + { + /** @var OrderInterface $order */ + $order = Bootstrap::getObjectManager()->create(Order::class)->load('100000001', 'increment_id'); + $orderItems = $order->getItems(); + $invoiceQtys = []; + foreach ($orderItems as $orderItem) { + $invoiceQtys[$orderItem->getItemId()] = $invoiceQty; + } + $invoice = $this->invoiceService->prepareInvoice($order, $invoiceQtys); + $invoiceItems = $invoice->getItems(); + foreach ($invoiceItems as $invoiceItem) { + $this->assertEquals($invoiceQty, $invoiceItem->getQty()); + } + } + + public function prepareInvoiceSimpleProductDataProvider() + { + return [ + 'full invoice' => [2], + 'partial invoice' => [1] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php new file mode 100644 index 0000000000000..1d03170d5b1e2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php @@ -0,0 +1,71 @@ +loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var \Magento\Catalog\Model\Product $product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product') + ->setSku('simple-product-guest-quote') + ->setPrice(10) + ->setTaxClassId(0) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); +$product = $productRepository->get('simple-product-guest-quote'); + +$addressData = reset($addresses); + +$billingAddress = $objectManager->create( + \Magento\Quote\Model\Quote\Address::class, + ['data' => $addressData] +); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$store = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore(); + +/** @var \Magento\Quote\Model\Quote $quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(true) + ->setStoreId($store->getId()) + ->setReservedOrderId('guest_quote') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product); +$quote->getPayment()->setMethod('checkmo'); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate')->setCollectShippingRates(true); +$quote->collectTotals(); + +$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMaskFactory::class)->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php new file mode 100644 index 0000000000000..e8992aec3c924 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php @@ -0,0 +1,43 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); + +$quote->load('guest_quote', 'reserved_order_id'); + +$quoteId = $quote->getId(); +if (null !== $quoteId) { + $quote->delete(); + $quoteIdMask->delete($quoteId); +} + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple-product-guest-quote', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +/** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\CatalogInventory\Model\StockRegistryStorage::class); +$stockRegistryStorage->clean(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php index a1c5f8277762c..6b9cf3bc613ce 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php @@ -44,7 +44,8 @@ ->setBasePrice($product->getPrice()) ->setPrice($product->getPrice()) ->setRowTotal($product->getPrice()) - ->setProductType('simple'); + ->setProductType('simple') + ->setName($product->getName()); /** @var Order $order */ $order = $objectManager->create(Order::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php index 251a384580062..99122d72df4b7 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php @@ -7,7 +7,9 @@ use Magento\Sales\Model\Order; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Payment; +// phpcs:ignore Magento2.Security.IncludeFile require 'order.php'; /** @var Order $order */ /** @var Order\Payment $payment */ @@ -23,8 +25,7 @@ 'subtotal' => 120.00, 'base_grand_total' => 120.00, 'store_id' => 1, - 'website_id' => 1, - 'payment' => $payment + 'website_id' => 1 ], [ 'increment_id' => '100000003', @@ -34,8 +35,7 @@ 'base_grand_total' => 140.00, 'subtotal' => 140.00, 'store_id' => 0, - 'website_id' => 0, - 'payment' => $payment + 'website_id' => 0 ], [ 'increment_id' => '100000004', @@ -45,8 +45,7 @@ 'base_grand_total' => 140.00, 'subtotal' => 140.00, 'store_id' => 1, - 'website_id' => 1, - 'payment' => $payment + 'website_id' => 1 ], ]; @@ -68,13 +67,26 @@ $shippingAddress = clone $billingAddress; $shippingAddress->setId(null)->setAddressType('shipping'); + /** @var Payment $payment */ + $payment = $objectManager->create(Payment::class); + $payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + $order ->setData($orderData) ->addItem($orderItem) ->setCustomerIsGuest(true) ->setCustomerEmail('customer@null.com') ->setBillingAddress($billingAddress) - ->setShippingAddress($shippingAddress); + ->setShippingAddress($shippingAddress) + ->setPayment($payment); $orderRepository->save($order); $orderList[] = $order; diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php index a83b01589ea9c..29a7aa4d90334 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php @@ -47,7 +47,9 @@ ->setProductType('simple') ->setDiscountAmount(2) ->setBaseRowTotal($product->getPrice()) - ->setBaseDiscountAmount(2); + ->setBaseDiscountAmount(2) + ->setTaxAmount(1) + ->setBaseTaxAmount(1); /** @var Order $order */ $order = $objectManager->create(Order::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon.php new file mode 100644 index 0000000000000..57ccffadaa4d0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon.php @@ -0,0 +1,35 @@ +create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setFreeShipping('1'); + +/** @var Order $order */ +$order->setShippingDescription('Flat Rate - Fixed') + ->setShippingAmount(0) + ->setCouponCode('1234567890') + ->setDiscountDescription('1234567890') + ->addItem($orderItem); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php new file mode 100644 index 0000000000000..1fb4b4636ab29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php @@ -0,0 +1,8 @@ +create(Status::class); +$data = [ + 'status' => 'custom_processing', + 'label' => 'Custom Processing Status', +]; +$orderStatus->setData($data)->setStatus('custom_processing'); +$orderStatus->save(); +$orderStatus->assignState('processing'); + +$order->setStatus('custom_processing'); +$order->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_and_custom_status_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_and_custom_status_rollback.php new file mode 100644 index 0000000000000..274cb3c74395d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_and_custom_status_rollback.php @@ -0,0 +1,17 @@ +create(Status::class); +$orderStatus->load('custom_processing', 'status'); +$orderStatus->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index b9ba89ba53144..2d0020ba22680 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -48,7 +48,22 @@ public static function loadByPhraseDataProvider() ['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0], ['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0] ] - ] + ], + [ + 'query_value', [] + ], + [ + 'query_value+', [] + ], + [ + 'query_value-', [] + ], + [ + 'query_@value', [] + ], + [ + 'query_value+@', [] + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php new file mode 100644 index 0000000000000..8794dfdff8fd7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php @@ -0,0 +1,171 @@ +accountManagement = $this->_objectManager->create(AccountManagementInterface::class); + $this->formKey = $this->_objectManager->create(FormKey::class); + $logger = $this->createMock(LoggerInterface::class); + $this->session = $this->_objectManager->create( + Session::class, + [$logger] + ); + $this->captchaHelper = $this->_objectManager->create(CaptchaHelper::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $this->session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testExecute() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + */ + public function testWithCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => 'test' + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + * + */ + public function testWithCaptchaSuccess() + { + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha('product_sendtofriend_form'); + $captchaModel->generate(); + $word = $captchaModel->getWord(); + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => $word + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php new file mode 100644 index 0000000000000..a075398e9cdb7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php @@ -0,0 +1,143 @@ +getProduct(); + $this->login(1); + $this->prepareRequestData(); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Share the product to friend as guest customer + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store sendfriend/email/enabled 1 + * @magentoConfigFixture default_store sendfriend/email/allow_guest 1 + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsGuest() + { + $product = $this->getProduct(); + $this->prepareRequestData(); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Share the product to friend as guest customer with invalid post data + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store sendfriend/email/enabled 1 + * @magentoConfigFixture default_store sendfriend/email/allow_guest 1 + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsGuestWithInvalidData() + { + $product = $this->getProduct(); + $this->prepareRequestData(true); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['Invalid Sender Email']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @return ProductInterface + */ + private function getProduct() + { + return $this->_objectManager->get(ProductRepositoryInterface::class)->get('custom-design-simple-product'); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = Bootstrap::getObjectManager() + ->get(Session::class); + $session->loginById($customerId); + } + + /** + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'sender' => [ + 'name' => 'Test', + 'email' => 'test@example.com', + 'message' => 'Message', + ], + 'recipients' => [ + 'name' => [ + 'Recipient 1', + 'Recipient 2' + ], + 'email' => [ + 'r1@example.com', + 'r2@example.com' + ] + ], + 'form_key' => $formKey->getFormKey(), + ]; + if ($invalidData) { + unset($post['sender']['email']); + } + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/process_config_data.php b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/process_config_data.php new file mode 100644 index 0000000000000..2c672378fb832 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/process_config_data.php @@ -0,0 +1,22 @@ + $value) { + $config->setDataByPath($key, $value); + $config->save(); + } +}; + +$deleteConfigData = function (WriterInterface $writer, array $configData, string $scope, int $scopeId) { + foreach ($configData as $path) { + $writer->delete($path, $scope, $scopeId); + } +}; diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration.php b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration.php new file mode 100644 index 0000000000000..229d6eddb496a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration.php @@ -0,0 +1,24 @@ + 1, + 'sendfriend/email/check_by' => 1, + +]; +$objectManager = Bootstrap::getObjectManager(); +$defConfig = $objectManager->create(Config::class); +$defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); +$processConfigData($defConfig, $configData); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration_rollback.php b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration_rollback.php new file mode 100644 index 0000000000000..9265e032bbc46 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration_rollback.php @@ -0,0 +1,22 @@ +get(WriterInterface::class); +$deleteConfigData($configWriter, $configData, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php b/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php new file mode 100644 index 0000000000000..202a396132485 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php @@ -0,0 +1,24 @@ +create(Value::class); +$config->setPath('sendfriend/email/enabled'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(1); +$config->save(); + +/** @var Value $config */ +$config = Bootstrap::getObjectManager()->create(Value::class); +$config->setPath('sendfriend/email/allow_guest'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(0); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/_files/product_simple_rollback.php b/dev/tests/integration/testsuite/Magento/SendFriend/_files/product_simple_rollback.php new file mode 100644 index 0000000000000..ed98732fc870e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/_files/product_simple_rollback.php @@ -0,0 +1,26 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php new file mode 100644 index 0000000000000..0a1926d58624c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php @@ -0,0 +1,92 @@ +transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return ShipmentInterface + */ + protected function getShipment(OrderInterface $order): ShipmentInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Collection $shipmentCollection */ + $shipmentCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Shipment\CollectionFactory::class + )->create(); + + /** @var ShipmentInterface $shipment */ + $shipment = $shipmentCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $shipment; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php new file mode 100644 index 0000000000000..25a44bab62994 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php @@ -0,0 +1,102 @@ +prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/admin/order_shipment/addComment'); + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject =__('Update to your %1 shipment', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment', ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment', ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $shipment = $this->getShipment($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $shipment->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php new file mode 100644 index 0000000000000..27b5bb02d4b22 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -0,0 +1,97 @@ +prepareRequest(['shipment' => ['send_email' => true]]); + $this->dispatch('backend/admin/order_shipment/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('The shipment has been created.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $shipment = $this->getShipment($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order has shipped', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $shipment->getStore()->getFrontendName() + ), + new StringContains( + "Your Shipment #{$shipment->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php b/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php index ebf302c16bd69..0e158821f1802 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php @@ -56,7 +56,7 @@ public function testHttpsPassSecureLoginPost() $this->prepareRequest(true); $this->dispatch('customer/account/loginPost/'); $redirectUrl = str_replace('http://', 'https://', $this->baseUrl) . - 'index.php/customer/account/'; + 'customer/account/'; $this->assertResponseRedirect($this->getResponse(), $redirectUrl); $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); $this->setFrontendCompletelySecureRollback(); diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index bb6d1687052e3..00de5544d8fb7 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -8,7 +8,6 @@ use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Bootstrap; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\UrlInterface; use Magento\Store\Api\StoreRepositoryInterface; @@ -16,6 +15,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Security.Superglobal */ class StoreTest extends \PHPUnit\Framework\TestCase { @@ -201,7 +201,7 @@ public function testGetBaseUrlInPub() */ public function testGetBaseUrlForCustomEntryPoint($type, $useCustomEntryPoint, $useStoreCode, $expected) { - /* config operations require store to be loaded */ + /* config operations require store to be loaded */ $this->model->load('default'); \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) @@ -213,6 +213,10 @@ public function testGetBaseUrlForCustomEntryPoint($type, $useCustomEntryPoint, $ // emulate custom entry point $_SERVER['SCRIPT_FILENAME'] = 'custom_entry.php'; + $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Framework\App\RequestInterface::class); + $request->setServer(new Parameters($_SERVER)); + if ($useCustomEntryPoint) { $property = new \ReflectionProperty($this->model, '_isCustomEntryPoint'); $property->setAccessible(true); @@ -298,11 +302,11 @@ public function testGetCurrentUrl() $url = $product->getUrlInStore(); $this->assertEquals( - $secondStore->getBaseUrl().'catalog/product/view/id/1/s/simple-product/', + $secondStore->getBaseUrl() . 'catalog/product/view/id/1/s/simple-product/', $url ); $this->assertEquals( - $secondStore->getBaseUrl().'?___from_store=default', + $secondStore->getBaseUrl() . '?___from_store=default', $secondStore->getCurrentUrl() ); $this->assertEquals( @@ -332,25 +336,25 @@ public function testGetCurrentUrlWithUseStoreInUrlFalse() $product->setStoreId($secondStore->getId()); $url = $product->getUrlInStore(); - /** @var \Magento\Catalog\Model\CategoryRepository $categoryRepository */ + /** @var \Magento\Catalog\Model\CategoryRepository $categoryRepository */ $categoryRepository = $objectManager->get(\Magento\Catalog\Model\CategoryRepository::class); $category = $categoryRepository->get(333, $secondStore->getStoreId()); $this->assertEquals( - $secondStore->getBaseUrl().'catalog/category/view/s/category-1/id/333/', + $secondStore->getBaseUrl() . 'catalog/category/view/s/category-1/id/333/', $category->getUrl() ); $this->assertEquals( - $secondStore->getBaseUrl(). + $secondStore->getBaseUrl() . 'catalog/product/view/id/333/s/simple-product-three/?___store=fixture_second_store', $url ); $this->assertEquals( - $secondStore->getBaseUrl().'?___store=fixture_second_store&___from_store=default', + $secondStore->getBaseUrl() . '?___store=fixture_second_store&___from_store=default', $secondStore->getCurrentUrl() ); $this->assertEquals( - $secondStore->getBaseUrl().'?___store=fixture_second_store', + $secondStore->getBaseUrl() . '?___store=fixture_second_store', $secondStore->getCurrentUrl(false) ); } @@ -405,7 +409,7 @@ public function testSaveValidation($badStoreData) /** * @return array */ - public static function saveValidationDataProvider() + public function saveValidationDataProvider() { return [ 'empty store name' => [['name' => '']], diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php index bd505fd4db035..985019b687ce0 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php @@ -7,10 +7,19 @@ namespace Magento\Tax\Model\Sales\Total\Quote; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Quote\Model\Quote; use Magento\Tax\Model\Config; use Magento\Tax\Model\Calculation; +use Magento\Quote\Model\Quote\Item\Updater; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; /** + * Setup utility for quote + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SetupUtil @@ -594,7 +603,7 @@ protected function createCartRule($ruleDataOverride) * * @param array $quoteData * @param \Magento\Customer\Api\Data\CustomerInterface $customer - * @return \Magento\Quote\Model\Quote + * @return Quote */ protected function createQuote($quoteData, $customer) { @@ -619,8 +628,8 @@ protected function createQuote($quoteData, $customer) $quoteBillingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); $quoteBillingAddress->importCustomerAddressData($addressService->getById($billingAddress->getId())); - /** @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->setStoreId(1) ->setIsActive(true) ->setIsMultiShipping(false) @@ -634,7 +643,7 @@ protected function createQuote($quoteData, $customer) /** * Add products to quote * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @param array $itemsData * @return $this */ @@ -657,7 +666,8 @@ protected function addProductToQuote($quote, $itemsData) * Create a quote based on given data * * @param array $quoteData - * @return \Magento\Quote\Model\Quote + * + * @return Quote */ public function setupQuote($quoteData) { @@ -666,7 +676,9 @@ public function setupQuote($quoteData) $quote = $this->createQuote($quoteData, $customer); $this->addProductToQuote($quote, $quoteData['items']); - + if (isset($quoteData['update_items'])) { + $this->updateItems($quote, $quoteData['update_items']); + } //Set shipping amount if (isset($quoteData['shipping_method'])) { $quote->getShippingAddress()->setShippingMethod($quoteData['shipping_method']); @@ -683,4 +695,33 @@ public function setupQuote($quoteData) return $quote; } + + /** + * Update quote items + * + * @param Quote $quote + * @param array $items + * + * @return void + */ + private function updateItems(Quote $quote, array $items): void + { + $updater = $this->objectManager->get(Updater::class); + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $filter = $this->objectManager->create(Filter::class); + $filter->setField('sku')->setValue(array_keys($items)); + $filterGroup = $this->objectManager->create(FilterGroup::class); + $filterGroup->setFilters([$filter]); + $searchCriteria = $this->objectManager->create(SearchCriteriaInterface::class); + $searchCriteria->setFilterGroups([$filterGroup]); + $products = $productRepository->getList($searchCriteria)->getItems(); + /** @var ProductInterface $product */ + foreach ($products as $product) { + $quoteItem = $quote->getItemByProduct($product); + $updater->update( + $quoteItem, + $items[$product->getSku()] + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php new file mode 100644 index 0000000000000..290c133f455f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php @@ -0,0 +1,93 @@ + [ + SetupUtil::CONFIG_OVERRIDES => [ + Config::CONFIG_XML_PATH_PRICE_INCLUDES_TAX => 1, + Config::CONFIG_XML_PATH_APPLY_ON => 0, + ], + SetupUtil::TAX_RATE_OVERRIDES => [ + SetupUtil::TAX_RATE_TX => 8.25, + SetupUtil::TAX_STORE_RATE => 8.25, + ], + SetupUtil::TAX_RULE_OVERRIDES => [ + ], + ], + 'quote_data' => [ + 'billing_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'shipping_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'items' => [ + [ + 'sku' => 'simple1', + 'price' => 16.24, + 'qty' => 1, + ], + ], + 'update_items' => [ + 'simple1' => [ + 'custom_price' => 14, + 'qty' => 1, + ], + ], + ], + 'expected_results' => [ + 'address_data' => [ + 'subtotal' => 12.93, + 'base_subtotal' => 12.93, + 'subtotal_incl_tax' => 14, + 'base_subtotal_incl_tax' => 14, + 'tax_amount' => 1.07, + 'base_tax_amount' => 1.07, + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'shipping_incl_tax' => 0, + 'base_shipping_incl_tax' => 0, + 'shipping_taxable' => 0, + 'base_shipping_taxable' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + 'grand_total' => 14, + 'base_grand_total' => 14, + ], + 'items_data' => [ + 'simple1' => [ + 'row_total' => 12.93, + 'base_row_total' => 12.93, + 'tax_percent' => 8.25, + 'price' => 12.93, + 'custom_price' => 12.93, + 'original_custom_price' => 14, + 'base_price' => 12.93, + 'price_incl_tax' => 14, + 'base_price_incl_tax' => 14, + 'row_total_incl_tax' => 14, + 'base_row_total_incl_tax' => 14, + 'tax_amount' => 1.07, + 'base_tax_amount' => 1.07, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_percent' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + ], + ], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php index f22b48a259685..3c56b1bf815a6 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php @@ -3,14 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); /** - * Global array that holds test scenarios data + * Global array that holds test scenarios data. * * @var array */ $taxCalculationData = []; - +//phpcs:disable Magento2.Security.IncludeFile require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_after_discount.php'; require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_after_discount_discount_tax.php'; require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_before_discount.php'; @@ -31,3 +32,4 @@ require_once __DIR__ . '/scenarios/multi_tax_rule_two_row_calculate_subtotal_yes_row.php'; require_once __DIR__ . '/scenarios/multi_tax_rule_two_row_calculate_subtotal_yes_total.php'; require_once __DIR__ . '/scenarios/including_tax_apply_tax_after_discount.php'; +require_once __DIR__ . '/scenarios/including_tax_with_custom_price.php'; diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index fe4067cdc49f5..042bd03b1cd4c 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Ups\Model; use Magento\TestFramework\Helper\Bootstrap; use Magento\Quote\Model\Quote\Address\RateRequestFactory; +/** + * Integration tests for Carrier model class + */ class CarrierTest extends \PHPUnit\Framework\TestCase { /** @@ -64,12 +69,12 @@ public function testGetShipConfirmUrlLive() /** * @magentoConfigFixture current_store carriers/ups/active 1 + * @magentoConfigFixture current_store carriers/ups/type UPS * @magentoConfigFixture current_store carriers/ups/allowed_methods 1DA,GND * @magentoConfigFixture current_store carriers/ups/free_method GND */ public function testCollectFreeRates() { - $this->markTestSkipped('Test is blocked by MAGETWO-97467.'); $rateRequest = Bootstrap::getObjectManager()->get(RateRequestFactory::class)->create(); $rateRequest->setDestCountryId('US'); $rateRequest->setDestRegionId('CA'); diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php index 2cb86358667c0..a8ff9e411785e 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php @@ -92,7 +92,8 @@ public function testSwitchToExistingPage(): void $storeRepository = $this->objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); $toStore = $storeRepository->get($toStoreCode); - $redirectUrl = $expectedUrl = "http://localhost/page-c"; + $redirectUrl = "http://localhost/index.php/page-c/"; + $expectedUrl = "http://localhost/index.php/page-c-on-2nd-store"; $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); } diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php new file mode 100644 index 0000000000000..b6055f14e79d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php @@ -0,0 +1,71 @@ +urlFinder = Bootstrap::getObjectManager()->create(UrlFinderInterface::class); + } + + /** + * @dataProvider findOneDataProvider + * @param string $requestPath + * @param string $targetPath + * @param int $redirectType + */ + public function testFindOneByData(string $requestPath, string $targetPath, int $redirectType) + { + $data = [ + UrlRewrite::REQUEST_PATH => $requestPath, + ]; + $urlRewrite = $this->urlFinder->findOneByData($data); + $this->assertEquals($targetPath, $urlRewrite->getTargetPath()); + $this->assertEquals($redirectType, $urlRewrite->getRedirectType()); + } + + /** + * @return array + */ + public function findOneDataProvider(): array + { + return [ + ['string', 'test_page1', 0], + ['string/', 'string', 301], + ['string_permanent', 'test_page1', 301], + ['string_permanent/', 'test_page1', 301], + ['string_temporary', 'test_page1', 302], + ['string_temporary/', 'test_page1', 302], + ['строка', 'test_page1', 0], + ['строка/', 'строка', 301], + [urlencode('строка'), 'test_page2', 0], + [urlencode('строка') . '/', urlencode('строка'), 301], + ['другая_строка', 'test_page1', 302], + ['другая_строка/', 'test_page1', 302], + [urlencode('другая_строка'), 'test_page1', 302], + [urlencode('другая_строка') . '/', 'test_page1', 302], + ['السلسلة', 'test_page1', 0], + [urlencode('السلسلة'), 'test_page1', 0], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php new file mode 100644 index 0000000000000..9edc6507308ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php @@ -0,0 +1,42 @@ +create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewrite::class); +foreach ($rewritesData as $rewriteData) { + list ($requestPath, $targetPath, $redirectType) = $rewriteData; + $rewrite = $objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); + $rewrite->setEntityType('custom') + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setRedirectType($redirectType); + $rewriteResource->save($rewrite); +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php new file mode 100644 index 0000000000000..a98f947d614e0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php @@ -0,0 +1,20 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$urlRewriteCollection = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection::class); +$collection = $urlRewriteCollection + ->addFieldToFilter('target_path', ['test_page1', 'test_page2']) + ->load() + ->walk('delete'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php index 672cbd7a586ec..937a26fdf0a89 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php @@ -89,10 +89,6 @@ public function testInvalidateTokenNoTokens() // invalidate token $this->getRequest()->setParam('user_id', $adminUserId); $this->dispatch('backend/admin/user/invalidateToken'); - $this->assertSessionMessages( - $this->equalTo(['This user has no tokens.']), - MessageInterface::TYPE_ERROR - ); } public function testInvalidateTokenNoUser() @@ -110,9 +106,5 @@ public function testInvalidateTokenInvalidUser() // invalidate token $this->getRequest()->setParam('user_id', $adminUserId); $this->dispatch('backend/admin/user/invalidateToken'); - $this->assertSessionMessages( - $this->equalTo(['This user has no tokens.']), - MessageInterface::TYPE_ERROR - ); } } diff --git a/dev/tests/integration/testsuite/Magento/Vault/_files/payment_tokens.php b/dev/tests/integration/testsuite/Magento/Vault/_files/payment_tokens.php index 2af563b35a399..f23a8fcd1bcfb 100644 --- a/dev/tests/integration/testsuite/Magento/Vault/_files/payment_tokens.php +++ b/dev/tests/integration/testsuite/Magento/Vault/_files/payment_tokens.php @@ -43,6 +43,22 @@ 'expires_at' => '2016-12-04 10:18:15', 'is_active' => 0 ], + [ + 'customer_id' => 1, + 'public_hash' => '34567', + 'payment_method_code' => 'fifth', + 'type' => 'card', + 'expires_at' => date('Y-m-d h:i:s', strtotime('+1 month')), + 'is_active' => 1 + ], + [ + 'customer_id' => 1, + 'public_hash' => '345678', + 'payment_method_code' => 'sixth', + 'type' => 'account', + 'expires_at' => date('Y-m-d h:i:s', strtotime('+1 month')), + 'is_active' => 1 + ], ]; /** @var array $tokenData */ foreach ($paymentTokens as $tokenData) { diff --git a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php index 5a07bdcfca35f..cc6f2793fcfef 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php @@ -38,6 +38,12 @@ public function testEditAction() public function testBlocksAction() { + \Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); + $theme = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\View\DesignInterface::class + )->setDefaultDesignTheme()->getDesignTheme(); + $this->getRequest()->setParam('theme_id', $theme->getId()); $this->dispatch('backend/admin/widget_instance/blocks'); $this->assertStringStartsWith(' - - -escapeCss('value') ?> -echo $var; -getHtmlId("some value") ?> -getIdHtml("some value") ?> -getIdHtml("some value") ?> - - - - - - - -methodHtml() ?> - -methodHtml() . - (bool)$var . - $block->escapeUrl("some value"); -?> -escapeHtml($data['parentSymbol']) . '\'' ?> -getExtendedElement($switchAttributeCode)->toHtml() ?> -escapeHtml($_filter->getFilter()->getClearLinkText()) ?> - - - tags from the code. */ -/* foreach ($block->getColumns() as $_column): ?> - getProperty() ?> /> - - -escapeHtmlAttr($block->getParamValue('title_' . $store['value'])) ?> diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_unsafe.phtml b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_unsafe.phtml deleted file mode 100644 index bf3aec540b815..0000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_unsafe.phtml +++ /dev/null @@ -1,29 +0,0 @@ - -getSomeData() ?> -getTitle() ?> -getSomeMethod($block->getHtmlId()) ?> - -escapeUrl($var) . $var . 'value' ?> - - - - -methodHtml() . - $var . - $block->getSomeData(); -?> -escapeHtml($data['parentSymbol']) . '\'' ?> -escapeHtml($data['parentSymbol']) . "\"" ?> - - tags from the code. */ -/* foreach ($block->getColumns() as $_column): ?> - getProperty() ?> /> - diff --git a/dev/tests/static/phpunit.xml.dist b/dev/tests/static/phpunit.xml.dist index 8ed80df002dfd..6c02ec1382563 100644 --- a/dev/tests/static/phpunit.xml.dist +++ b/dev/tests/static/phpunit.xml.dist @@ -24,9 +24,6 @@ testsuite/Magento/Test/Integrity - - testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php - diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php index 6d627574a3a18..b5a4e41b63279 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php @@ -194,7 +194,7 @@ private function assertClassesExist(array $classes, string $path): void foreach ($classes as $class) { $class = trim($class, '\\'); try { - if (strrchr($class, '\\') === false and !Classes::isVirtual($class)) { + if (strrchr($class, '\\') === false && !Classes::isVirtual($class)) { $badUsages[] = $class; continue; } else { diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php new file mode 100644 index 0000000000000..87cc5afd5ecb3 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php @@ -0,0 +1,179 @@ +readJsonFile($root . '/composer.json', true); + if (preg_match('/magento\/project-*/', $rootJson['name']) == 1) { + // The Dependency test is skipped for vendor/magento build + self::markTestSkipped( + 'MAGETWO-43654: The build is running from vendor/magento. DependencyTest is skipped.' + ); + } + $this->dependencyProvider = new DeclarativeSchemaDependencyProvider(); + } + + /** + * @throws \Exception + */ + public function testUndeclaredDependencies() + { + /** TODO: Remove this temporary solution after MC-15534 is closed */ + $filePattern = __DIR__ . '/_files/dependency_test/blacklisted_dependencies_*.php'; + $blacklistedDependencies = []; + foreach (glob($filePattern) as $fileName) { + $blacklistedDependencies = array_merge($blacklistedDependencies, require $fileName); + } + $this->blacklistedDependencies = $blacklistedDependencies; + + $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); + $invoker( + /** + * Check undeclared modules dependencies for specified file + * + * @param string $fileType + * @param string $file + */ + function ($file) { + $componentRegistrar = new ComponentRegistrar(); + $foundModuleName = ''; + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + if (strpos($file, $moduleDir . '/') !== false) { + $foundModuleName = str_replace('_', '\\', $moduleName); + break; + } + } + if (empty($foundModuleName)) { + return; + } + + $undeclaredDependency = $this->dependencyProvider->getUndeclaredModuleDependencies($foundModuleName); + + $result = []; + foreach ($undeclaredDependency as $name => $modules) { + $modules = array_unique($modules); + if ($this->filterBlacklistedDependencies($foundModuleName, $modules)) { + $result[] = $this->getErrorMessage($name) . "\n" . implode("\t\n", $modules) . "\n"; + } + } + if (!empty($result)) { + $this->fail( + 'Module ' . $moduleName . ' has undeclared dependencies: ' . "\n" . implode("\t\n", $result) + ); + } + }, + $this->prepareFiles(Files::init()->getDbSchemaFiles()) + ); + } + + /** + * Filter blacklisted dependencies. + * + * @todo Remove this temporary solution after MC-15534 is closed + * + * @param string $moduleName + * @param array $dependencies + * @return array + */ + private function filterBlacklistedDependencies(string $moduleName, array $dependencies): array + { + if (!empty($this->blacklistedDependencies[$moduleName])) { + $dependencies = array_diff($dependencies, $this->blacklistedDependencies[$moduleName]); + } + + return $dependencies; + } + + /** + * Convert file list to data provider structure. + * + * @param string[] $files + * @return array + */ + private function prepareFiles(array $files): array + { + $result = []; + foreach ($files as $relativePath => $file) { + $absolutePath = reset($file); + $result[$relativePath] = [$absolutePath]; + } + return $result; + } + + /** + * Retrieve error message for dependency. + * + * @param string $id + * @return string + */ + private function getErrorMessage(string $id): string + { + $decodedId = $this->dependencyProvider->decodeDependencyId($id); + $entityType = $decodedId['entityType']; + if ($entityType === DeclarativeSchemaDependencyProvider::SCHEMA_ENTITY_TABLE) { + $message = sprintf( + 'Table %s has undeclared dependency on one of the following modules:', + $decodedId['tableName'] + ); + } else { + $message = sprintf( + '%s %s from %s table has undeclared dependency on one of the following modules:', + ucfirst($entityType), + $decodedId['entityName'], + $decodedId['tableName'] + ); + } + + return $message; + } + + /** + * Read data from json file. + * + * @param string $file + * @return mixed + * @throws \Exception + */ + private function readJsonFile(string $file, bool $asArray = false) + { + $decodedJson = json_decode(file_get_contents($file), $asArray); + if (null == $decodedJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception("Invalid Json: $file"); + } + + return $decodedJson; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php new file mode 100644 index 0000000000000..965bc6184144b --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php @@ -0,0 +1,758 @@ +initDeclaredDependencies(); + $dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName)); + $dependencies = $this->filterSelfDependency($moduleName, $dependencies); + $declared = $this->getDeclaredDependencies($moduleName, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + + $existingDeclared = []; + foreach ($dependencies as $dependency) { + $checkResult = array_intersect($declared, $dependency); + if ($checkResult) { + $existingDeclared = array_merge($existingDeclared, array_values($checkResult)); + } + } + + return array_unique($existingDeclared); + } + + /** + * Provide undeclared dependencies between modules based on the declarative schema configuration. + * + * [ + * $dependencyId => [$module1, $module2, $module3 ...], + * ... + * ] + * + * @param string $moduleName + * @return array + * @throws \Exception + */ + public function getUndeclaredModuleDependencies(string $moduleName): array + { + $this->initDeclaredDependencies(); + $dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName)); + $dependencies = $this->filterSelfDependency($moduleName, $dependencies); + return $this->collectDependencies($moduleName, $dependencies); + } + + /** + * Provide schema file name by module name. + * + * @param string $module + * @return string + * @throws \Exception + */ + private function getSchemaFileNameByModuleName(string $module): string + { + if (empty($this->moduleSchemaFileMapping)) { + $componentRegistrar = new ComponentRegistrar(); + foreach (array_values(Files::init()->getDbSchemaFiles()) as $filePath) { + $filePath = reset($filePath); + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + if (strpos($filePath, $moduleDir . '/') !== false) { + $foundModuleName = str_replace('_', '\\', $moduleName); + $this->moduleSchemaFileMapping[$foundModuleName] = $filePath; + break; + } + } + } + } + + return $this->moduleSchemaFileMapping[$module] ?? ''; + } + + /** + * Initialise map of dependencies. + * + * @throws \Exception + */ + private function initDeclaredDependencies() + { + if (empty($this->mapDependencies)) { + $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); + foreach ($jsonFiles as $file) { + $json = new \Magento\Framework\Config\Composer\Package($this->readJsonFile($file)); + $moduleName = $this->convertModuleName($json->get('name')); + $require = array_keys((array)$json->get('require')); + $this->presetDependencies($moduleName, $require, self::TYPE_HARD); + } + } + } + + /** + * Read data from json file. + * + * @param string $file + * @return mixed + * @throws \Exception + */ + private function readJsonFile(string $file, bool $asArray = false) + { + $decodedJson = json_decode(file_get_contents($file), $asArray); + if (null == $decodedJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception("Invalid Json: $file"); + } + + return $decodedJson; + } + + /** + * Remove self dependencies. + * + * @param string $moduleName + * @param array $dependencies + * @return array + */ + private function filterSelfDependency(string $moduleName, array $dependencies):array + { + foreach ($dependencies as $id => $modules) { + $decodedId = $this->decodeDependencyId($id); + $entityType = $decodedId['entityType']; + if ($entityType === self::SCHEMA_ENTITY_TABLE || $entityType === "column") { + if (array_search($moduleName, $modules) !== false) { + unset($dependencies[$id]); + } + } else { + $dependencies[$id] = $this->filterComplexDependency($moduleName, $modules); + } + } + + return array_filter($dependencies); + } + + /** + * Remove already declared dependencies. + * + * @param string $moduleName + * @param array $modules + * @return array + */ + private function filterComplexDependency(string $moduleName, array $modules): array + { + $resultDependencies = []; + if (!is_array(reset($modules))) { + if (array_search($moduleName, $modules) === false) { + $resultDependencies = $modules; + } + } else { + foreach ($modules as $dependencySet) { + if (array_search($moduleName, $dependencySet) === false) { + $resultDependencies = array_merge( + $resultDependencies, + $dependencySet + ); + } + } + } + + return array_values(array_unique($resultDependencies)); + } + + /** + * Retrieve declarative schema declaration. + * + * @return array + * @throws \Exception + */ + private function getDeclarativeSchema(): array + { + if ($this->dbSchemaDeclaration) { + return $this->dbSchemaDeclaration; + } + + $entityTypes = [self::SCHEMA_ENTITY_COLUMN, self::SCHEMA_ENTITY_CONSTRAINT, self::SCHEMA_ENTITY_INDEX]; + $declaration = []; + foreach (Files::init()->getDbSchemaFiles() as $filePath) { + $filePath = reset($filePath); + preg_match('#app/code/(\w+/\w+)#', $filePath, $result); + $moduleName = str_replace('/', '\\', $result[1]); + $moduleDeclaration = $this->getDbSchemaDeclaration($filePath); + + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + if (!isset($tableDeclaration['modules'])) { + $tableDeclaration['modules'] = []; + } + array_push($tableDeclaration['modules'], $moduleName); + $moduleDeclaration = array_replace_recursive( + $moduleDeclaration, + [self::SCHEMA_ENTITY_TABLE => + [ + $tableName => $tableDeclaration, + ] + ] + ); + foreach ($entityTypes as $entityType) { + if (!isset($tableDeclaration[$entityType])) { + continue; + } + $moduleDeclaration = array_replace_recursive( + $moduleDeclaration, + [self::SCHEMA_ENTITY_TABLE => + [ + $tableName => + $this->addModuleAssigment($tableDeclaration, $entityType, $moduleName) + ] + ] + ); + } + } + $declaration = array_merge_recursive($declaration, $moduleDeclaration); + } + $this->dbSchemaDeclaration = $declaration; + + return $this->dbSchemaDeclaration; + } + + /** + * Get declared dependencies. + * + * @param string $tableName + * @param string $entityType + * @param null|string $entityName + * @return array + * @throws \Exception + */ + private function resolveEntityDependencies(string $tableName, string $entityType, ?string $entityName = null): array + { + switch ($entityType) { + case self::SCHEMA_ENTITY_COLUMN: + case self::SCHEMA_ENTITY_CONSTRAINT: + case self::SCHEMA_ENTITY_INDEX: + return $this->getDeclarativeSchema() + [self::SCHEMA_ENTITY_TABLE][$tableName][$entityType][$entityName]['modules']; + case self::SCHEMA_ENTITY_TABLE: + return $this->getDeclarativeSchema()[self::SCHEMA_ENTITY_TABLE][$tableName]['modules']; + default: + return []; + } + } + + /** + * @param string $filePath + * @return array + */ + private function getDbSchemaDeclaration(string $filePath): array + { + $dom = new \DOMDocument(); + $dom->loadXML(file_get_contents($filePath)); + return (new Converter())->convert($dom); + } + + /** + * Add dependency on the current module. + * + * @param array $tableDeclaration + * @param string $entityType + * @param string $moduleName + * @return array + */ + private function addModuleAssigment( + array $tableDeclaration, + string $entityType, + string $moduleName + ): array { + $declarationWithAssigment = []; + foreach ($tableDeclaration[$entityType] as $entityName => $entityDeclaration) { + if (!isset($entityDeclaration['modules'])) { + $entityDeclaration['modules'] = []; + } + if (!$this->isEntityDisabled($entityDeclaration)) { + array_push($entityDeclaration['modules'], $moduleName); + } + + $declarationWithAssigment[$entityType][$entityName] = $entityDeclaration; + } + + return $declarationWithAssigment; + } + + /** + * Retrieve dependencies from files. + * + * @param string $file + * @return string[] + * @throws \Exception + */ + private function getDependenciesFromFiles($file) + { + if (!$file) { + return []; + } + + $moduleDbSchema = $this->getDbSchemaDeclaration($file); + $dependencies = array_merge_recursive( + $this->getDisabledDependencies($moduleDbSchema), + $this->getConstraintDependencies($moduleDbSchema), + $this->getIndexDependencies($moduleDbSchema) + ); + return $dependencies; + } + + /** + * Retrieve dependencies for disabled entities. + * + * @param array $moduleDeclaration + * @return array + * @throws \Exception + */ + private function getDisabledDependencies(array $moduleDeclaration): array + { + $disabledDependencies = []; + $entityTypes = [self::SCHEMA_ENTITY_COLUMN, self::SCHEMA_ENTITY_CONSTRAINT, self::SCHEMA_ENTITY_INDEX]; + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + foreach ($entityTypes as $entityType) { + if (!isset($tableDeclaration[$entityType])) { + continue; + } + foreach ($tableDeclaration[$entityType] as $entityName => $entityDeclaration) { + if ($this->isEntityDisabled($entityDeclaration)) { + $dependencyIdentifier = $this->getDependencyId($tableName, $entityType, $entityName); + $disabledDependencies[$dependencyIdentifier] = + $this->resolveEntityDependencies($tableName, $entityType, $entityName); + } + } + } + if ($this->isEntityDisabled($tableDeclaration)) { + $disabledDependencies[$this->getDependencyId($tableName)] = + $this->resolveEntityDependencies($tableName, self::SCHEMA_ENTITY_TABLE); + } + } + + return $disabledDependencies; + } + + /** + * Retrieve dependencies for foreign entities. + * + * @param array $constraintDeclaration + * @return array + * @throws \Exception + */ + private function getFKDependencies(array $constraintDeclaration): array + { + $referenceDependencyIdentifier = + $this->getDependencyId( + $constraintDeclaration['referenceTable'], + self::SCHEMA_ENTITY_CONSTRAINT, + $constraintDeclaration['referenceId'] + ); + $dependencyIdentifier = + $this->getDependencyId( + $constraintDeclaration[self::SCHEMA_ENTITY_TABLE], + self::SCHEMA_ENTITY_CONSTRAINT, + $constraintDeclaration['referenceId'] + ); + + $constraintDependencies = []; + $constraintDependencies[$referenceDependencyIdentifier] = + $this->resolveEntityDependencies( + $constraintDeclaration['referenceTable'], + self::SCHEMA_ENTITY_COLUMN, + $constraintDeclaration['referenceColumn'] + ); + $constraintDependencies[$dependencyIdentifier] = + $this->resolveEntityDependencies( + $constraintDeclaration[self::SCHEMA_ENTITY_TABLE], + self::SCHEMA_ENTITY_COLUMN, + $constraintDeclaration[self::SCHEMA_ENTITY_COLUMN] + ); + + return $constraintDependencies; + } + + /** + * Retrieve dependencies for constraint entities. + * + * @param array $moduleDeclaration + * @return array + * @throws \Exception + */ + private function getConstraintDependencies(array $moduleDeclaration): array + { + $constraintDependencies = []; + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + if (empty($tableDeclaration[self::SCHEMA_ENTITY_CONSTRAINT])) { + continue; + } + foreach ($tableDeclaration[self::SCHEMA_ENTITY_CONSTRAINT] as $constraintName => $constraintDeclaration) { + if ($this->isEntityDisabled($constraintDeclaration)) { + continue; + } + $dependencyIdentifier = + $this->getDependencyId($tableName, self::SCHEMA_ENTITY_CONSTRAINT, $constraintName); + switch ($constraintDeclaration['type']) { + case 'foreign': + $constraintDependencies = array_merge( + $constraintDependencies, + $this->getFKDependencies($constraintDeclaration) + ); + break; + case 'primary': + case 'unique': + $constraintDependencies[$dependencyIdentifier] = $this->getComplexDependency( + $tableName, + $constraintDeclaration + ); + } + } + } + return $constraintDependencies; + } + + /** + * Calculate complex dependency. + * + * @param string $tableName + * @param array $entityDeclaration + * @return array + * @throws \Exception + */ + private function getComplexDependency(string $tableName, array $entityDeclaration): array + { + $complexDependency = []; + if (empty($entityDeclaration[self::SCHEMA_ENTITY_COLUMN])) { + return $complexDependency; + } + + if (!is_array($entityDeclaration[self::SCHEMA_ENTITY_COLUMN])) { + $entityDeclaration[self::SCHEMA_ENTITY_COLUMN] = [$entityDeclaration[self::SCHEMA_ENTITY_COLUMN]]; + } + + foreach (array_keys($entityDeclaration[self::SCHEMA_ENTITY_COLUMN]) as $columnName) { + $complexDependency[] = + $this->resolveEntityDependencies($tableName, self::SCHEMA_ENTITY_COLUMN, $columnName); + } + + return array_values($complexDependency); + } + + /** + * Retrieve dependencies for index entities. + * + * @param array $moduleDeclaration + * @return array + * @throws \Exception + */ + private function getIndexDependencies(array $moduleDeclaration): array + { + $indexDependencies = []; + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + if (empty($tableDeclaration[self::SCHEMA_ENTITY_INDEX])) { + continue; + } + foreach ($tableDeclaration[self::SCHEMA_ENTITY_INDEX] as $indexName => $indexDeclaration) { + if ($this->isEntityDisabled($indexDeclaration)) { + continue; + } + $dependencyIdentifier = + $this->getDependencyId($tableName, self::SCHEMA_ENTITY_INDEX, $indexName); + $indexDependencies[$dependencyIdentifier] = + $this->getComplexDependency($tableName, $indexDeclaration); + } + } + + return $indexDependencies; + } + + /** + * Check status of the entity declaration. + * + * @param array $entityDeclaration + * @return bool + */ + private function isEntityDisabled(array $entityDeclaration): bool + { + return isset($entityDeclaration['disabled']) && $entityDeclaration['disabled'] == true; + } + + /** + * Retrieve dependency id. + * + * @param string $tableName + * @param string $entityType + * @param null|string $entityName + * @return string + */ + private function getDependencyId( + string $tableName, + string $entityType = self::SCHEMA_ENTITY_TABLE, + ?string $entityName = null + ) { + return implode('___', [$tableName, $entityType, $entityName ?: $tableName]); + } + + /** + * Retrieve dependency parameters from dependency id. + * + * @param string $id + * @return array + */ + public static function decodeDependencyId(string $id): array + { + $decodedValues = explode('___', $id); + $result = [ + 'tableName' => $decodedValues[0], + 'entityType' => $decodedValues[1], + 'entityName' => $decodedValues[2], + ]; + return $result; + } + + /** + * Collect module dependencies. + * + * @param string $currentModuleName + * @param array $dependencies + * @return array + */ + private function collectDependencies($currentModuleName, $dependencies = []) + { + if (empty($dependencies)) { + return []; + } + foreach ($dependencies as $dependencyName => $dependency) { + $this->collectDependency($dependencyName, $dependency, $currentModuleName); + } + + return $this->getDeclaredDependencies($currentModuleName, self::TYPE_HARD, self::MAP_TYPE_FOUND); + } + + /** + * Collect a module dependency. + * + * @param string $dependencyName + * @param array $dependency + * @param string $currentModule + */ + private function collectDependency( + string $dependencyName, + array $dependency, + string $currentModule + ) { + $declared = $this->getDeclaredDependencies($currentModule, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + $checkResult = array_intersect($declared, $dependency); + + if (empty($checkResult)) { + $this->addDependencies( + $currentModule, + self::TYPE_HARD, + self::MAP_TYPE_FOUND, + [ + $dependencyName => $dependency, + ] + ); + } + } + + /** + * Add dependencies to dependency list. + * + * @param string $moduleName + * @param array $packageNames + * @param string $type + * + * @return void + * @throws \Exception + */ + private function presetDependencies( + string $moduleName, + array $packageNames, + string $type + ): void { + $packageNames = array_filter($packageNames, function ($packageName) { + return $this->getModuleName($packageName) || + 0 === strpos($packageName, 'magento/') && 'magento/magento-composer-installer' != $packageName; + }); + + foreach ($packageNames as $packageName) { + $this->addDependencies( + $moduleName, + $type, + self::MAP_TYPE_DECLARED, + [$this->convertModuleName($packageName)] + ); + } + } + + /** + * Returns package name on module name mapping. + * + * @return array + * @throws \Exception + */ + private function getPackageModuleMapping(): array + { + if (!$this->packageModuleMapping) { + $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); + + $packageModuleMapping = []; + foreach ($jsonFiles as $file) { + $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml'); + $moduleName = str_replace('_', '\\', (string)$moduleXml->module->attributes()->name); + $composerJson = $this->readJsonFile($file); + $packageName = $composerJson->name; + $packageModuleMapping[$packageName] = $moduleName; + } + + $this->packageModuleMapping = $packageModuleMapping; + } + + return $this->packageModuleMapping; + } + + /** + * Retrieve Magento style module name. + * + * @param string $packageName + * @return null|string + * @throws \Exception + */ + private function getModuleName(string $packageName): ?string + { + return $this->getPackageModuleMapping()[$packageName] ?? null; + } + + /** + * Retrieve array of dependency items. + * + * @param $module + * @param $type + * @param $mapType + * @return array + */ + private function getDeclaredDependencies(string $module, string $type, string $mapType) + { + return $this->mapDependencies[$module][$type][$mapType] ?? []; + } + + /** + * Add dependency map items. + * + * @param $module + * @param $type + * @param $mapType + * @param $dependencies + */ + protected function addDependencies(string $module, string $type, string $mapType, array $dependencies) + { + $this->mapDependencies[$module][$type][$mapType] = array_merge_recursive( + $this->getDeclaredDependencies($module, $type, $mapType), + $dependencies + ); + } + + /** + * Converts a composer json component name into the Magento Module form. + * + * @param string $jsonName The name of a composer json component or dependency e.g. 'magento/module-theme' + * @return string The corresponding Magento Module e.g. 'Magento\Theme' + * @throws \Exception + */ + private function convertModuleName(string $jsonName): string + { + $moduleName = $this->getModuleName($jsonName); + if ($moduleName) { + return $moduleName; + } + + if (strpos($jsonName, 'magento/magento') !== false + || strpos($jsonName, 'magento/framework') !== false + ) { + $moduleName = str_replace('/', "\t", $jsonName); + $moduleName = str_replace('framework-', "Framework\t", $moduleName); + $moduleName = str_replace('-', ' ', $moduleName); + $moduleName = ucwords($moduleName); + $moduleName = str_replace("\t", '\\', $moduleName); + $moduleName = str_replace(' ', '', $moduleName); + } else { + $moduleName = $jsonName; + } + + return $moduleName; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index a4113abed8030..9c03802602938 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -10,12 +10,13 @@ use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; use Magento\TestFramework\Dependency\DbRule; -use Magento\TestFramework\Dependency\DeclarativeSchemaRule; use Magento\TestFramework\Dependency\DiRule; use Magento\TestFramework\Dependency\LayoutRule; use Magento\TestFramework\Dependency\PhpRule; use Magento\TestFramework\Dependency\ReportsConfigRule; +use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; /** @@ -25,19 +26,28 @@ class DependencyTest extends \PHPUnit\Framework\TestCase { /** - * Types of dependencies between modules + * Soft dependency between modules */ const TYPE_SOFT = 'soft'; + /** + * Hard dependency between modules + */ const TYPE_HARD = 'hard'; /** - * Types of dependencies map arrays + * The identifier of dependency for mapping. */ const MAP_TYPE_DECLARED = 'declared'; + /** + * The identifier of dependency for mapping. + */ const MAP_TYPE_FOUND = 'found'; + /** + * The identifier of dependency for mapping. + */ const MAP_TYPE_REDUNDANT = 'redundant'; /** @@ -57,7 +67,7 @@ class DependencyTest extends \PHPUnit\Framework\TestCase protected static $_listConfigXml = []; /** - * List of db_schema.xml files by modules + * List of routes.xml files by modules * * Format: array( * '{Module_Name}' => '{Filename}' @@ -65,10 +75,10 @@ class DependencyTest extends \PHPUnit\Framework\TestCase * * @var array */ - protected static $_listDbSchemaXml = []; + protected static $_listRoutesXml = []; /** - * List of routes.xml files by modules + * List of analytics.xml * * Format: array( * '{Module_Name}' => '{Filename}' @@ -76,7 +86,7 @@ class DependencyTest extends \PHPUnit\Framework\TestCase * * @var array */ - protected static $_listRoutesXml = []; + protected static $_listAnalyticsXml = []; /** * List of routers @@ -174,8 +184,8 @@ public static function setUpBeforeClass() self::$_namespaces = implode('|', Files::init()->getNamespaces()); self::_prepareListConfigXml(); - self::_prepareListDbSchemaXml(); self::_prepareListRoutesXml(); + self::_prepareListAnalyticsXml(); self::_prepareMapRouters(); self::_prepareMapLayoutBlocks(); @@ -211,6 +221,7 @@ protected static function _initThemes() $defaultThemes = []; foreach (self::$_listConfigXml as $file) { $config = simplexml_load_file($file); + //phpcs:ignore Generic.PHP.NoSilencedErrors $nodes = @($config->xpath("/config/*/design/theme/full_name") ?: []); foreach ($nodes as $node) { $defaultThemes[] = (string)$node; @@ -224,13 +235,13 @@ protected static function _initThemes() */ protected static function _initRules() { - $replaceFilePattern = str_replace('\\', '/', realpath(__DIR__)) . '/_files/dependency_test/*.php'; + $replaceFilePattern = str_replace('\\', '/', realpath(__DIR__)) . '/_files/dependency_test/tables_*.php'; $dbRuleTables = []; foreach (glob($replaceFilePattern) as $fileName) { + //phpcs:ignore Generic.PHP.NoSilencedErrors $dbRuleTables = array_merge($dbRuleTables, @include $fileName); } self::$_rulesInstances = [ - new DeclarativeSchemaRule($dbRuleTables), new PhpRule(self::$_mapRouters, self::$_mapLayoutBlocks), new DbRule($dbRuleTables), new LayoutRule( @@ -240,6 +251,7 @@ protected static function _initRules() ), new DiRule(new VirtualTypeMapper()), new ReportsConfigRule($dbRuleTables), + new AnalyticsConfigRule(), ]; } @@ -261,7 +273,6 @@ protected function _getCleanedFileContents($fileType, $file) break; case 'layout': case 'config': - case 'db_schema': //Removing xml comments $contents = preg_replace('~\~s', '', $contents); break; @@ -285,6 +296,9 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { return $contents; } + /** + * @inheritdoc + */ public function testUndeclared() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); @@ -327,12 +341,12 @@ function ($fileType, $file) { $result = []; foreach ($undeclaredDependency as $type => $modules) { $modules = array_unique($modules); - if (!count($modules)) { + if (empty($modules)) { continue; } $result[] = sprintf("%s [%s]", $type, implode(', ', $modules)); } - if (count($result)) { + if (!empty($result)) { $this->fail('Module ' . $module . ' has undeclared dependencies: ' . implode(', ', $result)); } }, @@ -378,7 +392,7 @@ protected function getDependenciesFromFiles($module, $fileType, $file, $contents */ protected function _collectDependencies($currentModuleName, $dependencies = []) { - if (!count($dependencies)) { + if (empty($dependencies)) { return []; } $undeclared = []; @@ -416,18 +430,35 @@ private function collectDependency($dependency, $currentModule, &$undeclared) /** * Collect redundant dependencies + * * @SuppressWarnings(PHPMD.NPathComplexity) * @test * @depends testUndeclared + * @throws \Exception */ public function collectRedundant() { + $schemaDependencyProvider = new DeclarativeSchemaDependencyProvider(); + + /** TODO: Remove this temporary solution after MC-5806 is closed */ + $filePattern = __DIR__ . '/_files/dependency_test/undetected_dependencies_*.php'; + $undetectedDependencies = []; + foreach (glob($filePattern) as $fileName) { + $undetectedDependencies = array_merge($undetectedDependencies, require $fileName); + } + foreach (array_keys(self::$mapDependencies) as $module) { $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED); $found = array_merge( $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND), - $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND) + $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND), + $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module) ); + /** TODO: Remove this temporary solution after MC-5806 is closed */ + if (!empty($undetectedDependencies[$module])) { + $found = array_merge($found, $undetectedDependencies[$module]); + } + $found['Magento\Framework'] = 'Magento\Framework'; $this->_setDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT, array_diff($declared, $found)); } @@ -444,7 +475,7 @@ public function testRedundant() foreach (array_keys(self::$mapDependencies) as $module) { $result = []; $redundant = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT); - if (count($redundant)) { + if (!empty($redundant)) { $result[] = sprintf( "\r\nModule %s: %s [%s]", $module, @@ -453,11 +484,11 @@ public function testRedundant() ); } - if (count($result)) { + if (!empty($result)) { $output[] = implode(', ', $result); } } - if (count($output)) { + if (!empty($output)) { $this->fail("Redundant dependencies found!\r\n" . implode(' ', $output)); } } @@ -487,6 +518,7 @@ protected function _prepareFiles($fileType, $files, $skip = null) * Return all files * * @return array + * @throws \Exception */ public function getAllFiles() { @@ -508,12 +540,6 @@ public function getAllFiles() $this->_prepareFiles('config', Files::init()->getConfigFiles()) ); - // Get all configuration files - $files = array_merge( - $files, - $this->_prepareFiles('db_schema', Files::init()->getDbSchemaFiles()) - ); - //Get all layout updates files $files = array_merge( $files, @@ -544,29 +570,29 @@ protected static function _prepareListConfigXml() } /** - * Prepare list of db_schema.xml files (by modules) + * Prepare list of routes.xml files (by modules) */ - protected static function _prepareListDbSchemaXml() + protected static function _prepareListRoutesXml() { - $files = Files::init()->getDbSchemaFiles('db_schema.xml', [], false); + $files = Files::init()->getConfigFiles('*/routes.xml', [], false); foreach ($files as $file) { if (preg_match('/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches)) { $module = $matches['namespace'] . '\\' . $matches['module']; - self::$_listDbSchemaXml[$module] = $file; + self::$_listRoutesXml[$module][] = $file; } } } /** - * Prepare list of routes.xml files (by modules) + * Prepare list of analytics.xml files */ - protected static function _prepareListRoutesXml() + protected static function _prepareListAnalyticsXml() { - $files = Files::init()->getConfigFiles('*/routes.xml', [], false); + $files = Files::init()->getDbSchemaFiles('analytics.xml', [], false); foreach ($files as $file) { if (preg_match('/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches)) { $module = $matches['namespace'] . '\\' . $matches['module']; - self::$_listRoutesXml[$module][] = $file; + self::$_listAnalyticsXml[$module] = $file; } } } @@ -630,7 +656,7 @@ protected static function _prepareMapLayoutBlocks() $area = 'default'; if (preg_match('/[\/](?adminhtml|frontend)[\/]/', $file, $matches)) { $area = $matches['area']; - self::$_mapLayoutBlocks[$area] = @(self::$_mapLayoutBlocks[$area] ?: []); + self::$_mapLayoutBlocks[$area] = self::$_mapLayoutBlocks[$area] ?? []; } if (preg_match('/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches)) { $module = $matches['namespace'] . '\\' . $matches['module']; @@ -640,7 +666,7 @@ protected static function _prepareMapLayoutBlocks() $attributes = $element->attributes(); $block = (string)$attributes->name; if (!empty($block)) { - self::$_mapLayoutBlocks[$area][$block] = @(self::$_mapLayoutBlocks[$area][$block] ?: []); + self::$_mapLayoutBlocks[$area][$block] = self::$_mapLayoutBlocks[$area][$block] ?? []; self::$_mapLayoutBlocks[$area][$block][$module] = $module; } } @@ -658,7 +684,7 @@ protected static function _prepareMapLayoutHandles() $area = 'default'; if (preg_match('/\/(?adminhtml|frontend)\//', $file, $matches)) { $area = $matches['area']; - self::$_mapLayoutHandles[$area] = @(self::$_mapLayoutHandles[$area] ?: []); + self::$_mapLayoutHandles[$area] = self::$_mapLayoutHandles[$area] ?? []; } if (preg_match('/app\/code\/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches) ) { @@ -667,7 +693,7 @@ protected static function _prepareMapLayoutHandles() foreach ((array)$xml->xpath('/layout/child::*') as $element) { /** @var \SimpleXMLElement $element */ $handle = $element->getName(); - self::$_mapLayoutHandles[$area][$handle] = @(self::$_mapLayoutHandles[$area][$handle] ?: []); + self::$_mapLayoutHandles[$area][$handle] = self::$_mapLayoutHandles[$area][$handle] ?? []; self::$_mapLayoutHandles[$area][$handle][$module] = $module; } } @@ -727,6 +753,7 @@ protected static function _initDependencies() $contents = file_get_contents($file); $decodedJson = json_decode($contents); if (null == $decodedJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Invalid Json: $file"); } $json = new \Magento\Framework\Config\Composer\Package(json_decode($contents)); @@ -815,6 +842,7 @@ private static function getPackageModuleMapping(): array $contents = file_get_contents($file); $composerJson = json_decode($contents); if (null == $composerJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Invalid Json: $file"); } $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml'); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php index 4c0fed148aea8..f0177c449a3f9 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php @@ -102,8 +102,7 @@ public function testFileSchemaUsingInvalidXml($expectedErrors = null) Element 'metric', attribute 'type': [facet 'enumeration'] " . "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max', 'avg'}. Element 'metric', attribute 'type': 'sumasdasd' is not a valid value of the local atomic type. -Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges ). -Element 'request': Missing child element(s). Expected is ( from )." +Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges )." ) ); parent::testFileSchemaUsingInvalidXml($expectedErrors); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/blacklisted_dependencies_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/blacklisted_dependencies_ce.php new file mode 100644 index 0000000000000..270cb99c29caa --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/blacklisted_dependencies_ce.php @@ -0,0 +1,9 @@ + ["Magento\Inventory"], +]; diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php index 530f55504d009..3fb53be2ec400 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php @@ -96,7 +96,7 @@ 'catalogrule_website' => 'Magento\CatalogRule', 'catalogsearch_fulltext' => 'Magento\CatalogSearch', 'catalogsearch_result' => 'Magento\CatalogSearch', - 'search_query' => 'Magento\CatalogSearch', + 'search_query' => 'Magento\Search', 'checkout_agreement' => 'Magento\Checkout', 'checkout_agreement_store' => 'Magento\Checkout', 'cms_block' => 'Magento\Cms', diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/undetected_dependencies_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/undetected_dependencies_ce.php new file mode 100644 index 0000000000000..407f57ee51257 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/undetected_dependencies_ce.php @@ -0,0 +1,10 @@ + ["Magento\CatalogSearch"] +]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php index 6edc46090d545..2ce9934c6c1a2 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Test\Legacy; use Magento\Framework\App\Utility\Files; @@ -42,7 +44,6 @@ class UnsecureFunctionsUsageTest extends \PHPUnit\Framework\TestCase */ public static function setUpBeforeClass() { - self::loadData(self::$phpUnsecureFunctions, 'unsecure_php_functions*.php'); self::loadData(self::$jsUnsecureFunctions, 'unsecure_js_functions*.php'); } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php index 4ff5d0013892e..242e4ebb22a54 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php @@ -8,5 +8,6 @@ '/pub\/opt\/magento\/var/', '/COPYING\.txt/', '/setup\/src\/Zend\/Mvc\/Controller\/LazyControllerAbstractFactory\.php/', - '/app\/code\/(?!Magento)[^\/]*/' + '/app\/code\/(?!Magento)[^\/]*/', + '#dev/tests/setup-integration/testsuite/Magento/Developer/_files/\S*\.xml$#', ]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/security/unsecure_php_functions.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/security/unsecure_php_functions.php deleted file mode 100644 index 1c23f8d8ccf8a..0000000000000 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/security/unsecure_php_functions.php +++ /dev/null @@ -1,77 +0,0 @@ - will be suggested to be used instead. - * Use to specify files and directories that are allowed to use function. - * - * Format: [ - * => [ - * 'replacement' => , - * 'exclude' => [ - * , - * , - * ] - * ] - */ -return [ - 'unserialize' => [ - 'replacement' => '\Magento\Framework\Serialize\SerializerInterface::unserialize', - 'exclude' => [ - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'DB/Adapter/Pdo/Mysql.php'], - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'Serialize/Serializer/Serialize.php'], - ] - ], - 'serialize' => [ - 'replacement' => '\Magento\Framework\Serialize\SerializerInterface::serialize', - 'exclude' => [ - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'DB/Adapter/Pdo/Mysql.php'], - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'Serialize/Serializer/Serialize.php'], - ] - ], - 'eval' => [ - 'replacement' => '', - 'exclude' => [] - ], - 'md5' => [ - 'replacement' => '', - 'exclude' => [ - /* - * Usage of md5 in MessageQueue key generation algorithm - * added to exclude list to avoid backward incompatible changes - */ - [ - 'type' => 'library', - 'name' => 'magento/framework', - 'path' => 'MessageQueue/Rpc/Publisher.php', - ], - [ - 'type' => 'library', - 'name' => 'magento/framework', - 'path' => 'MessageQueue/MessageController.php', - ], - [ - 'type' => 'library', - 'name' => 'magento/framework', - 'path' => 'MessageQueue/Publisher.php', - ], - [ - 'type' => 'module', - 'name' => 'Magento_AsynchronousOperations', - 'path' => 'Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php' - ] - ] - ], - 'srand' => [ - 'replacement' => '', - 'exclude' => [] - ], - 'mt_srand' => [ - 'replacement' => '', - 'exclude' => [] - ], -]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml index 9bb00533a5da5..92e7b15efed29 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml @@ -69,5 +69,9 @@ dev/build/publication/sanity/ce.xml + + app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less + rma + diff --git a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php index 21ca0a495dd19..76c0d047bcbbf 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Test\Php; @@ -116,8 +117,10 @@ private static function getChangedFilesList($changedFilesBaseDir) 'changed_files*', function () { // if no list files, probably, this is the dev environment + // phpcs:disable Generic.PHP.NoSilencedErrors,Magento2.Security.InsecureFunction @exec('git diff --name-only', $changedFiles); @exec('git diff --cached --name-only', $addedFiles); + // phpcs:enable $changedFiles = array_unique(array_merge($changedFiles, $addedFiles)); return $changedFiles; } @@ -137,6 +140,7 @@ private static function getAddedFilesList($changedFilesBaseDir) 'changed_files*.added.*', function () { // if no list files, probably, this is the dev environment + // phpcs:ignore Generic.PHP.NoSilencedErrors,Magento2.Security.InsecureFunction @exec('git diff --cached --name-only', $addedFiles); return $addedFiles; } @@ -158,7 +162,7 @@ private static function getFilesFromListFile($listsBaseDir, $listFilePattern, $n $globFilesListPattern = ($listsBaseDir ?: self::getChangedFilesBaseDir()) . '/_files/' . $listFilePattern; $listFiles = glob($globFilesListPattern); - if (count($listFiles)) { + if (!empty($listFiles)) { foreach ($listFiles as $listFile) { $filesDefinedInList = array_merge( $filesDefinedInList, diff --git a/dev/tests/static/testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php b/dev/tests/static/testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php deleted file mode 100644 index fac14af5ecab8..0000000000000 --- a/dev/tests/static/testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php +++ /dev/null @@ -1,89 +0,0 @@ -{suffix}Html{postfix}() ). - * Data is ready for the HTML output. Test is green. - * 3. AbstractBlock methods escapeHtml, escapeUrl, escapeQuote, escapeXssInUrl are allowed. Test is green. - * 4. Type casting and php function count() are allowed - * (e.g. echo (int)$var, echo (float)$var, echo (bool)$var, echo count($var)). Test is green. - * 5. Output in single quotes (e.g. echo 'some text'). Test is green. - * 6. Output in double quotes without variables (e.g. echo "some text"). Test is green. - * 7. Other of p.1-6. Output is not escaped. Test is red. - * - * @param string $file - */ - function ($file) use ($xssOutputValidator) { - $lines = $xssOutputValidator->getLinesWithXssSensitiveOutput($file); - $this->assertEmpty( - $lines, - "Potentially XSS vulnerability. " . - "Please verify that output is escaped at lines " . $lines - ); - }, - Files::init()->getPhtmlFiles() - ); - } - - /** - * @return void - */ - public function testAbsenceOfEscapeNotVerifiedAnnotationInRefinedModules() - { - $componentRegistrar = new ComponentRegistrar(); - $exemptModules = []; - foreach (array_diff(scandir(__DIR__ . '/_files/whitelist/exempt_modules'), ['..', '.']) as $file) { - $exemptModules = array_merge( - $exemptModules, - include(__DIR__ . '/_files/whitelist/exempt_modules/' . $file) - ); - } - - $result = ""; - foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $modulePath) { - if (in_array($moduleName, $exemptModules)) { - continue; - } - foreach (Files::init()->getFiles([$modulePath], '*.phtml') as $file) { - $fileContents = file_get_contents($file); - $pattern = "/\\/* @escapeNotVerified \\*\\/ echo (?!__).+/"; - $instances = preg_grep($pattern, explode("\n", $fileContents)); - if (!empty($instances)) { - foreach (array_keys($instances) as $line) { - $result .= $file . ':' . ($line + 1) . "\n"; - } - } - } - } - $this->assertEmpty( - $result, - "@escapeNotVerified annotation detected.\n" . - "Please use the correct escape strategy and remove annotation at:\n" . $result - ); - } -} diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index 9c40f33f27a12..35ba5803b09cc 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt @@ -207,3 +207,10 @@ Magento/InventoryConfigurableProductIndexer/Indexer Magento/InventoryGroupedProductIndexer/Indexer Magento/Customer/Model/FileUploaderDataResolver.php Magento/Customer/Model/Customer/DataProvider.php +Magento/InventoryShippingAdminUi/Ui/DataProvider +Magento/Elasticsearch6/Model/Client +Magento/CatalogSearch/Model/ResourceModel/Fulltext +Magento/Elasticsearch/Model/Layer/Search +Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver +Magento/Elasticsearch6/Model/Client +Magento/Config/App/Config/Type diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml index 2d9eb7478ce91..0e3b5fa3d341c 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml @@ -29,9 +29,6 @@ - - - @@ -46,8 +43,7 @@ - - + diff --git a/dev/tests/unit/phpunit.xml.dist b/dev/tests/unit/phpunit.xml.dist index 102c9c41505e2..94500ff7bdc86 100644 --- a/dev/tests/unit/phpunit.xml.dist +++ b/dev/tests/unit/phpunit.xml.dist @@ -12,8 +12,10 @@ beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" > - + ../../../app/code/*/*/Test/Unit + + ../../../lib/internal/*/*/Test/Unit ../../../lib/internal/*/*/*/Test/Unit ../../../setup/src/*/*/Test/Unit diff --git a/lib/internal/LinLibertineFont/ChangeLog.txt b/lib/internal/LinLibertineFont/ChangeLog.txt index 8dc2c56567a4b..83b8792e71eda 100644 --- a/lib/internal/LinLibertineFont/ChangeLog.txt +++ b/lib/internal/LinLibertineFont/ChangeLog.txt @@ -952,7 +952,7 @@ Changes to version 0.5.8 regular(|) & italic(/) (20040315) Changes to version 0.5.7 regular(|) & italic(/) (20040315) -N is now 66pt wider -^ {Ascicircum} is now better -- {exclamdown} is now availible +- {exclamdown} is now available - {currency} has been added | "-" hyphen is the same as softhyphen. length is now 510pt -bars have been made diff --git a/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php b/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php index 49d824a4f2e5a..4dbf4680f8988 100644 --- a/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php +++ b/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php @@ -58,7 +58,7 @@ public function convertKeysToCamelCase(array $dataArray) if (is_array($fieldValue) && !$this->_isSimpleSequentialArray($fieldValue)) { $fieldValue = $this->convertKeysToCamelCase($fieldValue); } - $fieldName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $fieldName)))); + $fieldName = lcfirst(str_replace('_', '', ucwords($fieldName, '_'))); $response[$fieldName] = $fieldValue; } return $response; @@ -148,7 +148,7 @@ protected function _unpackAssociativeArray($data) */ public static function snakeCaseToUpperCamelCase($input) { - return str_replace(' ', '', ucwords(str_replace('_', ' ', $input))); + return str_replace('_', '', ucwords($input, '_')); } /** diff --git a/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php b/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php index d2f9b70913c1f..389bd8089967b 100644 --- a/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php +++ b/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php @@ -12,6 +12,8 @@ /** * Marker for actions processing HEAD requests. + * + * @deprecated Both GET and HEAD requests map to HttpGetActionInterface */ interface HttpHeadActionInterface extends ActionInterface { diff --git a/lib/internal/Magento/Framework/App/Bootstrap.php b/lib/internal/Magento/Framework/App/Bootstrap.php index 904c41ab9ec33..717b810cffd29 100644 --- a/lib/internal/Magento/Framework/App/Bootstrap.php +++ b/lib/internal/Magento/Framework/App/Bootstrap.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\App; @@ -245,6 +246,8 @@ public function createApplication($type, $arguments = []) * * @param \Magento\Framework\AppInterface $application * @return void + * + * phpcs:disable Magento2.Exceptions,Squiz.Commenting.FunctionCommentThrowTag */ public function run(AppInterface $application) { @@ -267,13 +270,15 @@ public function run(AppInterface $application) } catch (\Exception $e) { $this->terminate($e); } - } + } // phpcs:enable /** * Asserts maintenance mode * * @return void * @throws \Exception + * + * phpcs:disable Magento2.Exceptions */ protected function assertMaintenance() { @@ -299,7 +304,7 @@ protected function assertMaintenance() $this->errorCode = self::ERR_MAINTENANCE; throw new \Exception('Unable to proceed: the maintenance mode must be enabled first. '); } - } + } // phpcs:enable /** * Asserts whether application is installed @@ -316,10 +321,12 @@ protected function assertInstalled() $isInstalled = $this->isInstalled(); if (!$isInstalled && $isExpected) { $this->errorCode = self::ERR_IS_INSTALLED; + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Error: Application is not installed yet. '); } if ($isInstalled && !$isExpected) { $this->errorCode = self::ERR_IS_INSTALLED; + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Error: Application is already installed. '); } } @@ -413,10 +420,12 @@ public function isDeveloperMode() * * @param \Exception $e * @return void - * @SuppressWarnings(PHPMD.ExitExpression) + * + * phpcs:disable Magento2.Security.LanguageConstruct, Squiz.Commenting.FunctionCommentThrowTag */ protected function terminate(\Exception $e) { + if ($this->isDeveloperMode()) { echo $e; } else { @@ -433,4 +442,5 @@ protected function terminate(\Exception $e) } exit(1); } + // phpcs:enable } diff --git a/lib/internal/Magento/Framework/App/Console/Response.php b/lib/internal/Magento/Framework/App/Console/Response.php index 6255aaa3d87a6..853c3d5ca269e 100644 --- a/lib/internal/Magento/Framework/App/Console/Response.php +++ b/lib/internal/Magento/Framework/App/Console/Response.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\App\Console; /** - * @SuppressWarnings(PHPMD.ExitExpression) + * HTTP response implementation. */ class Response implements \Magento\Framework\App\ResponseInterface { @@ -53,9 +55,11 @@ class Response implements \Magento\Framework\App\ResponseInterface public function sendResponse() { if (!empty($this->body)) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $this->body; } if ($this->terminateOnSend) { + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit($this->code); } return $this->code; diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig.php b/lib/internal/Magento/Framework/App/DeploymentConfig.php index 615c295675adc..40b03b068d6ab 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig.php @@ -70,6 +70,11 @@ public function get($key = null, $defaultValue = null) if ($key === null) { return $this->flatData; } + + if (array_key_exists($key, $this->flatData) && $this->flatData[$key] === null) { + return ''; + } + return $this->flatData[$key] ?? $defaultValue; } @@ -146,6 +151,8 @@ private function load() } /** + * Array keys conversion + * * Convert associative array of arbitrary depth to a flat associative array with concatenated key path as keys * each level of array is accessible by path key * diff --git a/lib/internal/Magento/Framework/App/DocRootLocator.php b/lib/internal/Magento/Framework/App/DocRootLocator.php index 6fb35c42f1330..d73baf8e4e742 100644 --- a/lib/internal/Magento/Framework/App/DocRootLocator.php +++ b/lib/internal/Magento/Framework/App/DocRootLocator.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadFactory; /** @@ -20,18 +22,26 @@ class DocRootLocator private $request; /** + * @deprecated * @var ReadFactory */ private $readFactory; + /** + * @var Filesystem + */ + private $filesystem; + /** * @param RequestInterface $request * @param ReadFactory $readFactory + * @param Filesystem|null $filesystem */ - public function __construct(RequestInterface $request, ReadFactory $readFactory) + public function __construct(RequestInterface $request, ReadFactory $readFactory, Filesystem $filesystem = null) { $this->request = $request; $this->readFactory = $readFactory; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** @@ -42,7 +52,8 @@ public function __construct(RequestInterface $request, ReadFactory $readFactory) public function isPub() { $rootBasePath = $this->request->getServer('DOCUMENT_ROOT'); - $readDirectory = $this->readFactory->create(DirectoryList::ROOT); - return (substr($rootBasePath, -strlen('/pub')) === '/pub') && !$readDirectory->isExist($rootBasePath . 'setup'); + $readDirectory = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); + + return (substr($rootBasePath, -\strlen('/pub')) === '/pub') && ! $readDirectory->isExist('setup'); } } diff --git a/lib/internal/Magento/Framework/App/FrontControllerInterface.php b/lib/internal/Magento/Framework/App/FrontControllerInterface.php index a552d88e68f50..afd3091097d19 100644 --- a/lib/internal/Magento/Framework/App/FrontControllerInterface.php +++ b/lib/internal/Magento/Framework/App/FrontControllerInterface.php @@ -8,7 +8,7 @@ /** * Application front controller responsible for dispatching application requests. * Front controller contains logic common for all actions. - * Evary application area has own front controller + * Every application area has own front controller. * * @api */ diff --git a/lib/internal/Magento/Framework/App/Http.php b/lib/internal/Magento/Framework/App/Http.php index 3c6dee49f97b4..ca3976da1df52 100644 --- a/lib/internal/Magento/Framework/App/Http.php +++ b/lib/internal/Magento/Framework/App/Http.php @@ -3,16 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\App\Request\Http as RequestHttp; use Magento\Framework\App\Response\Http as ResponseHttp; use Magento\Framework\App\Response\HttpInterface; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Debug; use Magento\Framework\Event; use Magento\Framework\Filesystem; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; /** * HTTP web application. Called from webroot index.php to serve web requests. @@ -79,7 +81,7 @@ class Http implements \Magento\Framework\AppInterface * @param ResponseHttp $response * @param ConfigLoaderInterface $configLoader * @param State $state - * @param Filesystem $filesystem, + * @param Filesystem $filesystem * @param \Magento\Framework\Registry $registry */ public function __construct( @@ -142,6 +144,9 @@ public function launch() } else { throw new \InvalidArgumentException('Invalid return type'); } + if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) { + $this->handleHeadRequest(); + } // This event gives possibility to launch something before sending output (allow cookie setting) $eventParams = ['request' => $this->_request, 'response' => $this->_response]; $this->_eventManager->dispatch('controller_front_send_response_before', $eventParams); @@ -149,7 +154,23 @@ public function launch() } /** - * {@inheritdoc} + * Handle HEAD requests by adding the Content-Length header and removing the body from the response. + * + * @return void + */ + private function handleHeadRequest() + { + // It is possible that some PHP installations have overloaded strlen to use mb_strlen instead. + // This means strlen might return the actual number of characters in a non-ascii string instead + // of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure + // that the content length is calculated in bytes. + $contentLength = mb_strlen($this->_response->getContent(), '8bit'); + $this->_response->clearBody(); + $this->_response->setHeader('Content-Length', $contentLength); + } + + /** + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -198,6 +219,7 @@ private function buildContentFromException(\Exception $exception) { /** @var \Exception[] $exceptions */ $exceptions = []; + do { $exceptions[] = $exception; } while ($exception = $exception->getPrevious()); @@ -214,7 +236,12 @@ private function buildContentFromException(\Exception $exception) $index, get_class($exception), $exception->getMessage(), - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); } @@ -241,7 +268,7 @@ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) . "because the Magento setup directory cannot be accessed. \n" . 'You can install Magento using either the command line or you must restore access ' . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; - + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception($newMessage, 0, $exception); } } @@ -250,13 +277,14 @@ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) * Handler for bootstrap errors * * @param Bootstrap $bootstrap - * @param \Exception &$exception + * @param \Exception $exception * @return bool */ private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) { $bootstrapCode = $bootstrap->getErrorCode(); if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { + // phpcs:ignore Magento2.Security.IncludeFile require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); return true; } @@ -297,6 +325,7 @@ private function handleInitException(\Exception $exception) { if ($exception instanceof \Magento\Framework\Exception\State\InitException) { $this->getLogger()->critical($exception); + // phpcs:ignore Magento2.Security.IncludeFile require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); return true; } @@ -312,7 +341,15 @@ private function handleInitException(\Exception $exception) */ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) { - $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; + $reportData = [ + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ]; $params = $bootstrap->getParams(); if (isset($params['REQUEST_URI'])) { $reportData['url'] = $params['REQUEST_URI']; @@ -320,6 +357,7 @@ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception if (isset($params['SCRIPT_NAME'])) { $reportData['script_name'] = $params['SCRIPT_NAME']; } + // phpcs:ignore Magento2.Security.IncludeFile require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); return true; } diff --git a/lib/internal/Magento/Framework/App/MaintenanceMode.php b/lib/internal/Magento/Framework/App/MaintenanceMode.php index 4e4328cb72aef..e813522a01513 100644 --- a/lib/internal/Magento/Framework/App/MaintenanceMode.php +++ b/lib/internal/Magento/Framework/App/MaintenanceMode.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Event\Manager; /** * Application Maintenance Mode @@ -39,13 +40,18 @@ class MaintenanceMode protected $flagDir; /** - * Constructor - * + * @var Manager + */ + private $eventManager; + + /** * @param \Magento\Framework\Filesystem $filesystem + * @param Manager|null $eventManager */ - public function __construct(Filesystem $filesystem) + public function __construct(Filesystem $filesystem, ?Manager $eventManager = null) { $this->flagDir = $filesystem->getDirectoryWrite(self::FLAG_DIR); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(Manager::class); } /** @@ -73,6 +79,8 @@ public function isOn($remoteAddr = '') */ public function set($isOn) { + $this->eventManager->dispatch('maintenance_mode_changed', ['isOn' => $isOn]); + if ($isOn) { return $this->flagDir->touch(self::FLAG_FILENAME); } diff --git a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php index 19a89681a2d5f..d599f91ca8ca4 100644 --- a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php +++ b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php @@ -1,13 +1,17 @@ isFile($file)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception((string)new \Magento\Framework\Phrase('File not found')); } $contentLength = $dir->stat($file)['size']; @@ -86,6 +90,7 @@ public function create( if ($isFile) { $stream = $dir->openFile($file, 'r'); while (!$stream->eof()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $stream->read(1024); } } else { @@ -93,6 +98,7 @@ public function create( $file = $fileName; $stream = $dir->openFile($fileName, 'r'); while (!$stream->eof()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $stream->read(1024); } } diff --git a/lib/internal/Magento/Framework/App/Router/Base.php b/lib/internal/Magento/Framework/App/Router/Base.php index f810adcfd3491..fcce821858eb3 100644 --- a/lib/internal/Magento/Framework/App/Router/Base.php +++ b/lib/internal/Magento/Framework/App/Router/Base.php @@ -5,6 +5,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\App\Router; /** @@ -346,7 +348,6 @@ public function getActionClassName($module, $actionPath) * @param \Magento\Framework\App\RequestInterface $request * @param string $path * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ protected function _checkShouldBeSecure(\Magento\Framework\App\RequestInterface $request, $path = '') { @@ -361,6 +362,7 @@ protected function _checkShouldBeSecure(\Magento\Framework\App\RequestInterface } $this->_responseFactory->create()->setRedirect($url)->sendResponse(); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit; } } diff --git a/lib/internal/Magento/Framework/App/StaticResource.php b/lib/internal/Magento/Framework/App/StaticResource.php index 575074fdb58ac..86b2b15d3c446 100644 --- a/lib/internal/Magento/Framework/App/StaticResource.php +++ b/lib/internal/Magento/Framework/App/StaticResource.php @@ -10,6 +10,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Config\ConfigOptionsListConstants; use Psr\Log\LoggerInterface; +use Magento\Framework\Debug; /** * Entry point for retrieving static resources like JS, CSS, images by requested public path @@ -138,7 +139,7 @@ public function launch() } /** - * {@inheritdoc} + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -146,7 +147,15 @@ public function catchException(Bootstrap $bootstrap, \Exception $exception) if ($bootstrap->isDeveloperMode()) { $this->response->setHttpResponseCode(404); $this->response->setHeader('Content-Type', 'text/plain'); - $this->response->setBody($exception->getMessage() . "\n" . $exception->getTraceAsString()); + $this->response->setBody( + $exception->getMessage() . "\n" . + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ); $this->response->sendResponse(); } else { require $this->getFilesystem()->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php index 23afbbc73d2b9..ef4152ba2e49e 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php @@ -8,6 +8,9 @@ use Magento\Framework\App\DocRootLocator; +/** + * Test for Magento\Framework\App\DocRootLocator class. + */ class DocRootLocatorTest extends \PHPUnit\Framework\TestCase { /** @@ -21,11 +24,15 @@ public function testIsPub($path, $isExist, $result) { $request = $this->createMock(\Magento\Framework\App\Request\Http::class); $request->expects($this->once())->method('getServer')->willReturn($path); + + $readFactory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadFactory::class); + $reader = $this->createMock(\Magento\Framework\Filesystem\Directory\Read::class); + $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystem->expects($this->once())->method('getDirectoryRead')->willReturn($reader); $reader->expects($this->any())->method('isExist')->willReturn($isExist); - $readFactory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadFactory::class); - $readFactory->expects($this->once())->method('create')->willReturn($reader); - $model = new DocRootLocator($request, $readFactory); + + $model = new DocRootLocator($request, $readFactory, $filesystem); $this->assertSame($result, $model->isPub()); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php index a299e04e152cc..dbb315e88a526 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php @@ -92,7 +92,7 @@ protected function setUp() 'pathInfoProcessor' => $pathInfoProcessorMock, 'objectManager' => $objectManagerMock ]) - ->setMethods(['getFrontName']) + ->setMethods(['getFrontName', 'isHead']) ->getMock(); $this->areaListMock = $this->getMockBuilder(\Magento\Framework\App\AreaList::class) ->disableOriginalConstructor() @@ -135,12 +135,17 @@ private function setUpLaunch() { $frontName = 'frontName'; $areaCode = 'areaCode'; - $this->requestMock->expects($this->once())->method('getFrontName')->will($this->returnValue($frontName)); + $this->requestMock->expects($this->once()) + ->method('getFrontName') + ->willReturn($frontName); $this->areaListMock->expects($this->once()) ->method('getCodeByFrontName') - ->with($frontName)->will($this->returnValue($areaCode)); + ->with($frontName) + ->willReturn($areaCode); $this->configLoaderMock->expects($this->once()) - ->method('load')->with($areaCode)->will($this->returnValue([])); + ->method('load') + ->with($areaCode) + ->willReturn([]); $this->objectManagerMock->expects($this->once())->method('configure')->with([]); $this->objectManagerMock->expects($this->once()) ->method('get') @@ -149,12 +154,15 @@ private function setUpLaunch() $this->frontControllerMock->expects($this->once()) ->method('dispatch') ->with($this->requestMock) - ->will($this->returnValue($this->responseMock)); + ->willReturn($this->responseMock); } public function testLaunchSuccess() { $this->setUpLaunch(); + $this->requestMock->expects($this->once()) + ->method('isHead') + ->willReturn(false); $this->eventManagerMock->expects($this->once()) ->method('dispatch') ->with( @@ -171,33 +179,101 @@ public function testLaunchSuccess() public function testLaunchException() { $this->setUpLaunch(); - $this->frontControllerMock->expects($this->once())->method('dispatch')->with($this->requestMock)->will( - $this->returnCallback( - function () { - throw new \Exception('Message'); - } - ) - ); + $this->frontControllerMock->expects($this->once()) + ->method('dispatch') + ->with($this->requestMock) + ->willThrowException( + new \Exception('Message') + ); $this->http->launch(); } + /** + * Test that HEAD requests lead to an empty body and a Content-Length header matching the original body size. + * @dataProvider dataProviderForTestLaunchHeadRequest + * @param string $body + * @param int $expectedLength + */ + public function testLaunchHeadRequest($body, $expectedLength) + { + $this->setUpLaunch(); + $this->requestMock->expects($this->once()) + ->method('isHead') + ->willReturn(true); + $this->responseMock->expects($this->once()) + ->method('getHttpResponseCode') + ->willReturn(200); + $this->responseMock->expects($this->once()) + ->method('getContent') + ->willReturn($body); + $this->responseMock->expects($this->once()) + ->method('clearBody') + ->willReturn($this->responseMock); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Length', $expectedLength) + ->willReturn($this->responseMock); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'controller_front_send_response_before', + ['request' => $this->requestMock, 'response' => $this->responseMock] + ); + $this->assertSame($this->responseMock, $this->http->launch()); + } + + /** + * Different test content for responseMock with their expected lengths in bytes. + * @return array + */ + public function dataProviderForTestLaunchHeadRequest(): array + { + return [ + [ + "Test", // Ascii text + 43 // Expected Content-Length + ], + [ + "部落格", // Multi-byte characters + 48 // Expected Content-Length + ], + [ + "\0", // Null byte + 40 // Expected Content-Length + ], + [ + "خرید", // LTR text + 47 // Expected Content-Length + ] + ]; + } + public function testHandleDeveloperModeNotInstalled() { $dir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $dir->expects($this->once())->method('getAbsolutePath')->willReturn(__DIR__); + $dir->expects($this->once()) + ->method('getAbsolutePath') + ->willReturn(__DIR__); $this->filesystemMock->expects($this->once()) ->method('getDirectoryRead') ->with(DirectoryList::ROOT) ->willReturn($dir); - $this->responseMock->expects($this->once())->method('setRedirect')->with('/_files/'); - $this->responseMock->expects($this->once())->method('sendHeaders'); + $this->responseMock->expects($this->once()) + ->method('setRedirect') + ->with('/_files/'); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); $bootstrap = $this->getBootstrapNotInstalled(); - $bootstrap->expects($this->once())->method('getParams')->willReturn([ - 'SCRIPT_NAME' => '/index.php', - 'DOCUMENT_ROOT' => __DIR__, - 'SCRIPT_FILENAME' => __DIR__ . '/index.php', - SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files', - ]); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn( + [ + 'SCRIPT_NAME' => '/index.php', + 'DOCUMENT_ROOT' => __DIR__, + 'SCRIPT_FILENAME' => __DIR__ . '/index.php', + SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files', + ] + ); $this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test Message'))); } @@ -206,24 +282,37 @@ public function testHandleDeveloperMode() $this->filesystemMock->expects($this->once()) ->method('getDirectoryRead') ->will($this->throwException(new \Exception('strange error'))); - $this->responseMock->expects($this->once())->method('setHttpResponseCode')->with(500); - $this->responseMock->expects($this->once())->method('setHeader')->with('Content-Type', 'text/plain'); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(500); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 'text/plain'); $constraint = new \PHPUnit\Framework\Constraint\StringStartsWith('1 exception(s):'); - $this->responseMock->expects($this->once())->method('setBody')->with($constraint); - $this->responseMock->expects($this->once())->method('sendResponse'); + $this->responseMock->expects($this->once()) + ->method('setBody') + ->with($constraint); + $this->responseMock->expects($this->once()) + ->method('sendResponse'); $bootstrap = $this->getBootstrapNotInstalled(); - $bootstrap->expects($this->once())->method('getParams')->willReturn( - ['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else'] - ); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn( + ['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else'] + ); $this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test'))); } public function testCatchExceptionSessionException() { - $this->responseMock->expects($this->once())->method('setRedirect'); - $this->responseMock->expects($this->once())->method('sendHeaders'); + $this->responseMock->expects($this->once()) + ->method('setRedirect'); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); $bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class); - $bootstrap->expects($this->once())->method('isDeveloperMode')->willReturn(false); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(false); $this->assertTrue($this->http->catchException( $bootstrap, new \Magento\Framework\Exception\SessionException(new \Magento\Framework\Phrase('Test')) @@ -238,8 +327,12 @@ public function testCatchExceptionSessionException() private function getBootstrapNotInstalled() { $bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class); - $bootstrap->expects($this->once())->method('isDeveloperMode')->willReturn(true); - $bootstrap->expects($this->once())->method('getErrorCode')->willReturn(Bootstrap::ERR_IS_INSTALLED); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(true); + $bootstrap->expects($this->once()) + ->method('getErrorCode') + ->willReturn(Bootstrap::ERR_IS_INSTALLED); return $bootstrap; } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php index 5d1c22a38af4d..5970d2561660a 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php @@ -6,9 +6,17 @@ namespace Magento\Framework\App\Test\Unit; -use \Magento\Framework\App\MaintenanceMode; +use Magento\Framework\App\MaintenanceMode; +use Magento\Framework\Event\Manager; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Filesystem; +use PHPUnit\Framework\TestCase; -class MaintenanceModeTest extends \PHPUnit\Framework\TestCase +/** + * MaintenanceMode Test + */ +class MaintenanceModeTest extends TestCase { /** * @var MaintenanceMode @@ -16,141 +24,213 @@ class MaintenanceModeTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\Filesystem\Directory\WriteInterface | \PHPUnit_Framework_MockObject_MockObject + * @var WriteInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $flagDir; + /** + * @var Manager|\PHPUnit\Framework\MockObject\MockObject + */ + private $eventManager; + + /** + * @inheritdoc + */ protected function setup() { - $this->flagDir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\WriteInterface::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $filesystem->expects($this->any()) - ->method('getDirectoryWrite') - ->will($this->returnValue($this->flagDir)); + $this->flagDir = $this->getMockForAbstractClass(WriteInterface::class); + $filesystem = $this->createMock(Filesystem::class); + $filesystem->method('getDirectoryWrite') + ->willReturn($this->flagDir); + $this->eventManager = $this->createMock(Manager::class); - $this->model = new MaintenanceMode($filesystem); + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject(MaintenanceMode::class, [ + 'filesystem' => $filesystem, + 'eventManager' => $this->eventManager, + ]); } + /** + * Is On initial test + * + * @return void + */ public function testIsOnInitial() { - $this->flagDir->expects($this->once())->method('isExist') + $this->flagDir->expects($this->once()) + ->method('isExist') ->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); + ->willReturn(false); $this->assertFalse($this->model->isOn()); } + /** + * Is On without ip test + * + * @return void + */ public function testisOnWithoutIP() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, false], ]; - $this->flagDir->expects($this->exactly(2))->method('isExist') - ->will(($this->returnValueMap($mapisExist))); + $this->flagDir->expects($this->exactly(2)) + ->method('isExist') + ->willReturnMap($mapisExist); $this->assertTrue($this->model->isOn()); } + /** + * Is On with IP test + * + * @return void + */ public function testisOnWithIP() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->exactly(2))->method('isExist') - ->will(($this->returnValueMap($mapisExist))); + $this->flagDir->expects($this->exactly(2)) + ->method('isExist') + ->willReturnMap($mapisExist); $this->assertFalse($this->model->isOn()); } + /** + * Is On with IP but no Maintenance files test + * + * @return void + */ public function testisOnWithIPNoMaintenance() { - $this->flagDir->expects($this->once())->method('isExist') + $this->flagDir->expects($this->once()) + ->method('isExist') ->with(MaintenanceMode::FLAG_FILENAME) ->willReturn(false); $this->assertFalse($this->model->isOn()); } + /** + * Maintenance Mode On test + * + * Tests common scenario with Full Page Cache is set to On + * + * @return void + */ public function testMaintenanceModeOn() { - $this->flagDir->expects($this->at(0))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - $this->flagDir->expects($this->at(1))->method('touch')->will($this->returnValue(true)); - $this->flagDir->expects($this->at(2))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(true)); - $this->flagDir->expects($this->at(3))->method('isExist')->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue(false)); + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with('maintenance_mode_changed', ['isOn' => true]); - $this->assertFalse($this->model->isOn()); - $this->assertTrue($this->model->set(true)); - $this->assertTrue($this->model->isOn()); + $this->flagDir->expects($this->once()) + ->method('touch') + ->with(MaintenanceMode::FLAG_FILENAME); + + $this->model->set(true); } + /** + * Maintenance Mode Off test + * + * Tests common scenario when before Maintenance Mode Full Page Cache was setted to on + * + * @return void + */ public function testMaintenanceModeOff() { - $this->flagDir->expects($this->at(0))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(true)); - $this->flagDir->expects($this->at(1))->method('delete')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - $this->flagDir->expects($this->at(2))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - - $this->assertFalse($this->model->set(false)); - $this->assertFalse($this->model->isOn()); + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with('maintenance_mode_changed', ['isOn' => false]); + + $this->flagDir->method('isExist') + ->with(MaintenanceMode::FLAG_FILENAME) + ->willReturn(true); + + $this->flagDir->expects($this->once()) + ->method('delete') + ->with(MaintenanceMode::FLAG_FILENAME); + + $this->model->set(false); } + /** + * Set empty addresses test + * + * @return void + */ public function testSetAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('writeFile') + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('writeFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue(true)); + ->willReturn(true); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('')); + ->willReturn(''); $this->model->setAddresses(''); $this->assertEquals([''], $this->model->getAddressInfo()); } + /** + * Set single address test + * + * @return void + */ public function testSetSingleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('writeFile') - ->will($this->returnValue(10)); + $this->flagDir->method('writeFile') + ->willReturn(10); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1')); + ->willReturn('address1'); $this->model->setAddresses('address1'); $this->assertEquals(['address1'], $this->model->getAddressInfo()); } + /** + * Is On when multiple addresses test was setted + * + * @return void + */ public function testOnSetMultipleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('writeFile') - ->will($this->returnValue(10)); + $this->flagDir->method('writeFile') + ->willReturn(10); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1,10.50.60.123')); + ->willReturn('address1,10.50.60.123'); $expectedArray = ['address1', '10.50.60.123']; $this->model->setAddresses('address1,10.50.60.123'); @@ -159,18 +239,25 @@ public function testOnSetMultipleAddresses() $this->assertTrue($this->model->isOn('address3')); } + /** + * Is Off when multiple addresses test was setted + * + * @return void + */ public function testOffSetMultipleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, false], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1,10.50.60.123')); + ->willReturn('address1,10.50.60.123'); $expectedArray = ['address1', '10.50.60.123']; $this->model->setAddresses('address1,10.50.60.123'); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php index 3d44073a24b85..94a7330c322b8 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php @@ -7,6 +7,9 @@ */ namespace Magento\Framework\App\Test\Unit\Router; +/** + * Base router unit test. + */ class BaseTest extends \Magento\Framework\TestFramework\Unit\BaseTestCase { /** @@ -83,11 +86,13 @@ public function testMatch() $actionClassName = \Magento\Framework\App\Action\Action::class; $moduleName = 'module name'; $moduleList = [$moduleName]; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(true); $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); @@ -140,6 +145,7 @@ public function testMatchUseDefaultPath() $actionClassName = \Magento\Framework\App\Action\Action::class; $moduleName = 'module name'; $moduleList = [$moduleName]; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $defaultReturnMap = [ @@ -147,6 +153,7 @@ public function testMatchUseDefaultPath() ['controller', $actionPath], ['action', $actionName], ]; + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->defaultPathMock->expects($this->any())->method('getPart')->willReturnMap($defaultReturnMap); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(false); @@ -171,9 +178,11 @@ public function testMatchEmptyModuleList() $actionName = 'action name'; $actionClassName = \Magento\Framework\App\Action\Action::class; $emptyModuleList = []; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($emptyModuleList); $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); @@ -195,9 +204,11 @@ public function testMatchEmptyActionInstance() $actionClassName = \Magento\Framework\App\Action\Action::class; $moduleName = 'module name'; $moduleList = [$moduleName]; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); diff --git a/lib/internal/Magento/Framework/Cache/InvalidateLogger.php b/lib/internal/Magento/Framework/Cache/InvalidateLogger.php index 10886f911e295..08f9930a81b2f 100644 --- a/lib/internal/Magento/Framework/Cache/InvalidateLogger.php +++ b/lib/internal/Magento/Framework/Cache/InvalidateLogger.php @@ -10,6 +10,9 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Psr\Log\LoggerInterface as Logger; +/** + * Invalidate logger cache. + */ class InvalidateLogger { /** @@ -34,6 +37,7 @@ public function __construct(HttpRequest $request, Logger $logger) /** * Logger invalidate cache + * * @param mixed $invalidateInfo * @return void */ @@ -44,6 +48,7 @@ public function execute($invalidateInfo) /** * Make extra data to logger message + * * @param mixed $invalidateInfo * @return array */ @@ -65,4 +70,16 @@ public function critical($message, $params) { $this->logger->critical($message, $this->makeParams($params)); } + + /** + * Log warning + * + * @param string $message + * @param mixed $params + * @return void + */ + public function warning($message, $params) + { + $this->logger->warning($message, $this->makeParams($params)); + } } diff --git a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php new file mode 100644 index 0000000000000..216d8e9a0a01b --- /dev/null +++ b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php @@ -0,0 +1,116 @@ +locker = $locker; + $this->lockTimeout = $lockTimeout; + $this->delayTimeout = $delayTimeout; + } + + /** + * Load data. + * + * @param string $lockName + * @param callable $dataLoader + * @param callable $dataCollector + * @param callable $dataSaver + * @return mixed + */ + public function lockedLoadData( + string $lockName, + callable $dataLoader, + callable $dataCollector, + callable $dataSaver + ) { + $cachedData = $dataLoader(); //optimistic read + + while ($cachedData === false && $this->locker->isLocked($lockName)) { + usleep($this->delayTimeout * 1000); + $cachedData = $dataLoader(); + } + + while ($cachedData === false) { + try { + if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + $data = $dataCollector(); + $dataSaver($data); + $cachedData = $data; + } + } finally { + $this->locker->unlock($lockName); + } + + if ($cachedData === false) { + usleep($this->delayTimeout * 1000); + $cachedData = $dataLoader(); + } + } + + return $cachedData; + } + + /** + * Clean data. + * + * @param string $lockName + * @param callable $dataCleaner + * @return void + */ + public function lockedCleanData(string $lockName, callable $dataCleaner) + { + while ($this->locker->isLocked($lockName)) { + usleep($this->delayTimeout * 1000); + } + try { + if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + $dataCleaner(); + } + } finally { + $this->locker->unlock($lockName); + } + } +} diff --git a/lib/internal/Magento/Framework/Code/Generator.php b/lib/internal/Magento/Framework/Code/Generator.php index 4dec7d1a28146..b46c8c681bb52 100644 --- a/lib/internal/Magento/Framework/Code/Generator.php +++ b/lib/internal/Magento/Framework/Code/Generator.php @@ -8,11 +8,15 @@ use Magento\Framework\Code\Generator\DefinedClasses; use Magento\Framework\Code\Generator\EntityAbstract; use Magento\Framework\Code\Generator\Io; +use Magento\Framework\ObjectManager\ConfigInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; use Magento\Framework\Filesystem\Driver\File; use Psr\Log\LoggerInterface; +/** + * Class code generator. + */ class Generator { const GENERATION_SUCCESS = 'success'; @@ -232,7 +236,21 @@ protected function shouldSkipGeneration($resultEntityType, $sourceClassName, $re { if (!$resultEntityType || !$sourceClassName) { return self::GENERATION_ERROR; - } elseif ($this->definedClasses->isClassLoadableFromDisk($resultClass)) { + } + + /** @var ConfigInterface $omConfig */ + $omConfig = $this->objectManager->get(ConfigInterface::class); + $virtualTypes = $omConfig->getVirtualTypes(); + + /** + * Do not try to autogenerate virtual types + * For example virtual types with names overlapping autogenerated suffixes + */ + if (isset($virtualTypes[$resultClass])) { + return self::GENERATION_SKIP; + } + + if ($this->definedClasses->isClassLoadableFromDisk($resultClass)) { $generatedFileName = $this->_ioObject->generateResultFileName($resultClass); /** * Must handle two edge cases: a competing process has generated the class and written it to disc already, @@ -244,9 +262,12 @@ protected function shouldSkipGeneration($resultEntityType, $sourceClassName, $re $this->_ioObject->includeFile($generatedFileName); } return self::GENERATION_SKIP; - } elseif (!isset($this->_generatedEntities[$resultEntityType])) { + } + + if (!isset($this->_generatedEntities[$resultEntityType])) { throw new \InvalidArgumentException('Unknown generation entity.'); } + return false; } } diff --git a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php index c214008393609..35c138147e9d3 100644 --- a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php +++ b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php @@ -3,37 +3,94 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Code\Generator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Code\Generator; +use Psr\Log\LoggerInterface; +/** + * Class loader and generator. + */ class Autoloader { /** - * @var \Magento\Framework\Code\Generator + * @var Generator */ protected $_generator; /** - * @param \Magento\Framework\Code\Generator $generator + * Enables guarding against spamming the debug log with duplicate messages, as + * the generation exception will be thrown multiple times within a single request. + * + * @var string + */ + private $lastGenerationErrorMessage; + + /** + * @param Generator $generator */ - public function __construct( - \Magento\Framework\Code\Generator $generator - ) { + public function __construct(Generator $generator) + { $this->_generator = $generator; } /** * Load specified class name and generate it if necessary * + * According to PSR-4 section 2.4 an autoloader MUST NOT throw an exception and SHOULD NOT return a value. + * + * @see https://www.php-fig.org/psr/psr-4/ + * * @param string $className - * @return bool True if class was loaded + * @return void */ public function load($className) { - if (!class_exists($className)) { - return Generator::GENERATION_ERROR != $this->_generator->generateClass($className); + if (! class_exists($className)) { + try { + $this->_generator->generateClass($className); + } catch (\Exception $exception) { + $this->tryToLogExceptionMessageIfNotDuplicate($exception); + } + } + } + + /** + * Log exception. + * + * @param \Exception $exception + */ + private function tryToLogExceptionMessageIfNotDuplicate(\Exception $exception): void + { + if ($this->lastGenerationErrorMessage !== $exception->getMessage()) { + $this->lastGenerationErrorMessage = $exception->getMessage(); + $this->tryToLogException($exception); + } + } + + /** + * Try to capture the exception message. + * + * The Autoloader is instantiated before the ObjectManager, so the LoggerInterface can not be injected. + * The Logger is instantiated in the try/catch block because ObjectManager might still not be initialized. + * In that case the exception message can not be captured. + * + * The debug level is used for logging in case class generation fails for a common class, but a custom + * autoloader is used later in the stack. A more severe log level would fill the logs with messages on production. + * The exception message now can be accessed in developer mode if debug logging is enabled. + * + * @param \Exception $exception + * @return void + */ + private function tryToLogException(\Exception $exception): void + { + try { + $logger = ObjectManager::getInstance()->get(LoggerInterface::class); + $logger->debug($exception->getMessage(), ['exception' => $exception]); + } catch (\Exception $ignoreThisException) { + // Do not take an action here, since the original exception might have been caused by logger } - return true; } } diff --git a/lib/internal/Magento/Framework/Code/NameBuilder.php b/lib/internal/Magento/Framework/Code/NameBuilder.php index c27a896b65f04..993235054e490 100644 --- a/lib/internal/Magento/Framework/Code/NameBuilder.php +++ b/lib/internal/Magento/Framework/Code/NameBuilder.php @@ -1,12 +1,15 @@ definedClassesMock = $this->createMock(DefinedClasses::class); @@ -65,6 +88,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerConfigMock = $this->getMockBuilder(ConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->model = new Generator( $this->ioObjectMock, @@ -78,7 +107,7 @@ protected function setUp() ); } - public function testGetGeneratedEntities() + public function testGetGeneratedEntities(): void { $this->model = new Generator( $this->ioObjectMock, @@ -91,22 +120,58 @@ public function testGetGeneratedEntities() /** * @param string $className * @param string $entityType - * @expectedException \RuntimeException + * @expectedException RuntimeException * @dataProvider generateValidClassDataProvider */ - public function testGenerateClass($className, $entityType) + public function testGenerateClass($className, $entityType): void { - $objectManagerMock = $this->createMock(ObjectManagerInterface::class); $fullClassName = $className . $entityType; + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock->expects($this->once())->method('create')->willReturn($entityGeneratorMock); - $this->model->setObjectManager($objectManagerMock); - $this->model->generateClass($fullClassName); + $this->objectManagerMock + ->expects($this->once()) + ->method('create') + ->willReturn($entityGeneratorMock); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SUCCESS, + $this->model->generateClass($fullClassName) + ); } - public function testGenerateClassWithWrongName() + public function testShouldNotGenerateVirtualType(): void + { + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([GeneratedClassFactory::class => GeneratedClassFactory::class]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SKIP, + $this->model->generateClass(GeneratedClassFactory::class) + ); + } + + public function testGenerateClassWithWrongName(): void { $this->assertEquals( Generator::GENERATION_ERROR, @@ -115,25 +180,42 @@ public function testGenerateClassWithWrongName() } /** - * @expectedException \RuntimeException + * @expectedException RuntimeException */ - public function testGenerateClassWhenClassIsNotGenerationSuccess() + public function testGenerateClassWhenClassIsNotGenerationSuccess(): void { $expectedEntities = array_values($this->expectedEntities); $resultClassName = self::SOURCE_CLASS . ucfirst(array_shift($expectedEntities)); - $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock->expects($this->once())->method('create')->willReturn($entityGeneratorMock); - $this->model->setObjectManager($objectManagerMock); - $this->model->generateClass($resultClassName); + $this->objectManagerMock + ->expects($this->once()) + ->method('create') + ->willReturn($entityGeneratorMock); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SUCCESS, + $this->model->generateClass($resultClassName) + ); } /** * @inheritdoc */ - public function testGenerateClassWithErrors() + public function testGenerateClassWithErrors(): void { $expectedEntities = array_values($this->expectedEntities); $resultClassName = self::SOURCE_CLASS . ucfirst(array_shift($expectedEntities)); @@ -148,17 +230,15 @@ public function testGenerateClassWithErrors() . 'directory permission is set to write --- the requested class did not generate properly, then ' . 'you must add the generated class object to the signature of the related construct method, only.'; $FinalErrorMessage = implode(PHP_EOL, $errorMessages) . "\n" . $mainErrorMessage; - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage($FinalErrorMessage); - /** @var ObjectManagerInterface|Mock $objectManagerMock */ - $objectManagerMock = $this->createMock(ObjectManagerInterface::class); /** @var EntityAbstract|Mock $entityGeneratorMock */ $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock->expects($this->once()) + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($entityGeneratorMock); $entityGeneratorMock->expects($this->once()) @@ -177,26 +257,62 @@ public function testGenerateClassWithErrors() $this->loggerMock->expects($this->once()) ->method('critical') ->with($FinalErrorMessage); - $this->model->setObjectManager($objectManagerMock); - $this->model->generateClass($resultClassName); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SUCCESS, + $this->model->generateClass($resultClassName) + ); } /** * @dataProvider trueFalseDataProvider + * @param $fileExists */ - public function testGenerateClassWithExistName($fileExists) + public function testGenerateClassWithExistName($fileExists): void { $this->definedClassesMock->expects($this->any()) ->method('isClassLoadableFromDisk') ->willReturn(true); $resultClassFileName = '/Magento/Path/To/Class.php'; - $this->ioObjectMock->expects($this->once())->method('generateResultFileName')->willReturn($resultClassFileName); - $this->ioObjectMock->expects($this->once())->method('fileExists')->willReturn($fileExists); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->ioObjectMock + ->expects($this->once()) + ->method('generateResultFileName') + ->willReturn($resultClassFileName); + $this->ioObjectMock + ->expects($this->once()) + ->method('fileExists') + ->willReturn($fileExists); + $includeFileInvokeCount = $fileExists ? 1 : 0; - $this->ioObjectMock->expects($this->exactly($includeFileInvokeCount))->method('includeFile'); + $this->ioObjectMock + ->expects($this->exactly($includeFileInvokeCount)) + ->method('includeFile'); - $this->assertEquals( + $this->assertSame( Generator::GENERATION_SKIP, $this->model->generateClass(GeneratedClassFactory::class) ); @@ -205,7 +321,7 @@ public function testGenerateClassWithExistName($fileExists) /** * @return array */ - public function trueFalseDataProvider() + public function trueFalseDataProvider(): array { return [[true], [false]]; } @@ -215,7 +331,7 @@ public function trueFalseDataProvider() * * @return array */ - public function generateValidClassDataProvider() + public function generateValidClassDataProvider(): array { $data = []; foreach ($this->expectedEntities as $generatedEntity) { diff --git a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php index 8fcbd74c884d9..b79ba49a24ddd 100644 --- a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php +++ b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php @@ -124,20 +124,22 @@ protected function extractTopics($config) $requestSchema, $responseSchema ); + $isSynchronous = $this->extractTopicIsSynchronous($topicNode); if ($serviceMethod) { $output[$topicName] = $this->reflectionGenerator->generateTopicConfigForServiceMethod( $topicName, $serviceMethod[ConfigParser::TYPE_NAME], $serviceMethod[ConfigParser::METHOD_NAME], - $handlers + $handlers, + $isSynchronous ); } elseif ($requestSchema && $responseSchema) { $output[$topicName] = [ Config::TOPIC_NAME => $topicName, - Config::TOPIC_IS_SYNCHRONOUS => true, + Config::TOPIC_IS_SYNCHRONOUS => $isSynchronous, Config::TOPIC_REQUEST => $requestSchema, Config::TOPIC_REQUEST_TYPE => Config::TOPIC_REQUEST_TYPE_CLASS, - Config::TOPIC_RESPONSE => $responseSchema, + Config::TOPIC_RESPONSE => ($isSynchronous) ? $responseSchema: null, Config::TOPIC_HANDLERS => $handlers ]; } elseif ($requestSchema) { @@ -258,4 +260,20 @@ protected function parseServiceMethod($serviceMethod, $topicName) ); return $parsedServiceMethod; } + + /** + * Extract is_synchronous topic value. + * + * @param \DOMNode $topicNode + * @return bool + */ + private function extractTopicIsSynchronous($topicNode): bool + { + $attributeName = Config::TOPIC_IS_SYNCHRONOUS; + $topicAttributes = $topicNode->attributes; + if (!$topicAttributes->getNamedItem($attributeName)) { + return true; + } + return $this->booleanUtils->toBoolean($topicAttributes->getNamedItem($attributeName)->nodeValue); + } } diff --git a/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php b/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php index d1bc62464f212..7ef84f1c43b10 100644 --- a/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php +++ b/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php @@ -42,7 +42,10 @@ public function extractMethodMetadata($className, $methodName) $result = [ Config::SCHEMA_METHOD_PARAMS => [], Config::SCHEMA_METHOD_RETURN_TYPE => $this->methodsMap->getMethodReturnType($className, $methodName), - Config::SCHEMA_METHOD_HANDLER => [Config::HANDLER_TYPE => $className, Config::HANDLER_METHOD => $methodName] + Config::SCHEMA_METHOD_HANDLER => [ + Config::HANDLER_TYPE => $className, + Config::HANDLER_METHOD => $methodName + ] ]; $paramsMeta = $this->methodsMap->getMethodParams($className, $methodName); foreach ($paramsMeta as $paramPosition => $paramMeta) { @@ -63,16 +66,27 @@ public function extractMethodMetadata($className, $methodName) * @param string $serviceType * @param string $serviceMethod * @param array|null $handlers + * @param bool|null $isSynchronous * @return array */ - public function generateTopicConfigForServiceMethod($topicName, $serviceType, $serviceMethod, $handlers = []) - { + public function generateTopicConfigForServiceMethod( + $topicName, + $serviceType, + $serviceMethod, + $handlers = [], + $isSynchronous = null + ) { $methodMetadata = $this->extractMethodMetadata($serviceType, $serviceMethod); $returnType = $methodMetadata[Config::SCHEMA_METHOD_RETURN_TYPE]; $returnType = ($returnType != 'void' && $returnType != 'null') ? $returnType : null; + if (!isset($isSynchronous)) { + $isSynchronous = $returnType ? true : false; + } else { + $returnType = ($isSynchronous) ? $returnType : null; + } return [ Config::TOPIC_NAME => $topicName, - Config::TOPIC_IS_SYNCHRONOUS => $returnType ? true : false, + Config::TOPIC_IS_SYNCHRONOUS => $isSynchronous, Config::TOPIC_REQUEST => $methodMetadata[Config::SCHEMA_METHOD_PARAMS], Config::TOPIC_REQUEST_TYPE => Config::TOPIC_REQUEST_TYPE_METHOD, Config::TOPIC_RESPONSE => $returnType, @@ -85,7 +99,8 @@ public function generateTopicConfigForServiceMethod($topicName, $serviceType, $s * Generate topic name based on service type and method name. * * Perform the following conversion: - * \Magento\Customer\Api\RepositoryInterface + getById => magento.customer.api.repositoryInterface.getById + * \Magento\Customer\Api\RepositoryInterface + getById => + * magento.customer.api.repositoryInterface.getById * * @param string $typeName * @param string $methodName diff --git a/lib/internal/Magento/Framework/Communication/etc/communication.xsd b/lib/internal/Magento/Framework/Communication/etc/communication.xsd index 12ee56371ce77..678d89f30c531 100644 --- a/lib/internal/Magento/Framework/Communication/etc/communication.xsd +++ b/lib/internal/Magento/Framework/Communication/etc/communication.xsd @@ -40,6 +40,7 @@ + diff --git a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php index 92f0302d93baf..6bdb74ef7b89a 100644 --- a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php +++ b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php @@ -47,7 +47,7 @@ class ConfigOptionsListConstants const CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION = 'static_content_on_demand_in_production'; /** - * Paramater for forcing HTML minification even if file is already minified. + * Parameter for forcing HTML minification even if file is already minified. */ const CONFIG_PATH_FORCE_HTML_MINIFICATION = 'force_html_minification'; diff --git a/lib/internal/Magento/Framework/Config/Dom.php b/lib/internal/Magento/Framework/Config/Dom.php index 1d995bab007e5..5c97c996634dd 100644 --- a/lib/internal/Magento/Framework/Config/Dom.php +++ b/lib/internal/Magento/Framework/Config/Dom.php @@ -190,9 +190,20 @@ protected function _mergeNode(\DOMElement $node, $parentPath) /* override node value */ if ($this->_isTextNode($node)) { /* skip the case when the matched node has children, otherwise they get overridden */ - if (!$matchedNode->hasChildNodes() || $this->_isTextNode($matchedNode)) { + if (!$matchedNode->hasChildNodes() + || $this->_isTextNode($matchedNode) + || $this->isCdataNode($matchedNode) + ) { $matchedNode->nodeValue = $node->childNodes->item(0)->nodeValue; } + } elseif ($this->isCdataNode($node) && $this->_isTextNode($matchedNode)) { + /* Replace text node with CDATA section */ + if ($this->findCdataSection($node)) { + $matchedNode->nodeValue = $this->findCdataSection($node)->nodeValue; + } + } elseif ($this->isCdataNode($node) && $this->isCdataNode($matchedNode)) { + /* Replace CDATA with new one */ + $this->replaceCdataNode($matchedNode, $node); } else { /* recursive merge for all child nodes */ foreach ($node->childNodes as $childNode) { @@ -220,6 +231,56 @@ protected function _isTextNode($node) return $node->childNodes->length == 1 && $node->childNodes->item(0) instanceof \DOMText; } + /** + * Check if the node content is CDATA (probably surrounded with text nodes) or just text node + * + * @param \DOMNode $node + * @return bool + */ + private function isCdataNode($node) + { + // If every child node of current is NOT \DOMElement + // It is arbitrary combination of text nodes and CDATA sections. + foreach ($node->childNodes as $childNode) { + if ($childNode instanceof \DOMElement) { + return false; + } + } + + return true; + } + + /** + * Finds CDATA section from given node children + * + * @param \DOMNode $node + * @return \DOMCdataSection|null + */ + private function findCdataSection($node) + { + foreach ($node->childNodes as $childNode) { + if ($childNode instanceof \DOMCdataSection) { + return $childNode; + } + } + } + + /** + * Replaces CDATA section in $oldNode with $newNode's + * + * @param \DOMNode $oldNode + * @param \DOMNode $newNode + */ + private function replaceCdataNode($oldNode, $newNode) + { + $oldCdata = $this->findCdataSection($oldNode); + $newCdata = $this->findCdataSection($newNode); + + if ($oldCdata && $newCdata) { + $oldCdata->nodeValue = $newCdata->nodeValue; + } + } + /** * Merges attributes of the merge node to the base node * @@ -318,7 +379,7 @@ public static function validateDomDocument( libxml_set_external_entity_loader([self::$urnResolver, 'registerEntityLoader']); $errors = []; try { - $result = $dom->schemaValidate($schema); + $result = $dom->schemaValidate($schema, LIBXML_SCHEMA_CREATE); if (!$result) { $errors = self::getXmlErrors($errorFormat); } diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php index 5c8f66683877c..73968ac1ed897 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php +++ b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Config\Test\Unit; +/** + * Test for \Magento\Framework\Config\Dom class. + */ class DomTest extends \PHPUnit\Framework\TestCase { /** @@ -62,6 +65,37 @@ public function mergeDataProvider() ['override_node.xml', 'override_node_new.xml', [], null, 'override_node_merged.xml'], ['override_node_new.xml', 'override_node.xml', [], null, 'override_node_merged.xml'], ['text_node.xml', 'text_node_new.xml', [], null, 'text_node_merged.xml'], + 'text node replaced with cdata' => [ + 'text_node_cdata.xml', + 'text_node_cdata_new.xml', + [], + null, + 'text_node_cdata_merged.xml' + ], + 'cdata' => ['cdata.xml', 'cdata_new.xml', [], null, 'cdata_merged.xml'], + 'cdata with html' => ['cdata_html.xml', 'cdata_html_new.xml', [], null, 'cdata_html_merged.xml'], + 'cdata replaced with text node' => [ + 'cdata_text.xml', + 'cdata_text_new.xml', + [], + null, + 'cdata_text_merged.xml' + ], + 'big cdata' => ['big_cdata.xml', 'big_cdata_new.xml', [], null, 'big_cdata_merged.xml'], + 'big cdata with attribute' => [ + 'big_cdata_attribute.xml', + 'big_cdata_attribute_new.xml', + [], + null, + 'big_cdata_attribute_merged.xml' + ], + 'big cdata replaced with text' => [ + 'big_cdata_text.xml', + 'big_cdata_text_new.xml', + [], + null, + 'big_cdata_text_merged.xml' + ], [ 'recursive.xml', 'recursive_new.xml', @@ -135,6 +169,48 @@ public function validateDataProvider() ]; } + /** + * @param string $xml + * @param string $expectedValue + * @dataProvider validateWithDefaultValueDataProvider + */ + public function testValidateWithDefaultValue($xml, $expectedValue) + { + if (!function_exists('libxml_set_external_entity_loader')) { + $this->markTestSkipped('Skipped on HHVM. Will be fixed in MAGETWO-45033'); + } + + $actualErrors = []; + + $dom = new \Magento\Framework\Config\Dom($xml, $this->validationStateMock); + $dom->validate(__DIR__ . '/_files/sample.xsd', $actualErrors); + + $actualValue = $dom->getDom() + ->getElementsByTagName('root')->item(0) + ->getElementsByTagName('node')->item(0) + ->getAttribute('attribute_with_default_value'); + + $this->assertEmpty($actualErrors); + $this->assertEquals($expectedValue, $actualValue); + } + + /** + * @return array + */ + public function validateWithDefaultValueDataProvider() + { + return [ + 'default_value' => [ + '', + 'default_value' + ], + 'custom_value' => [ + '', + 'non_default_value' + ], + ]; + } + public function testValidateCustomErrorFormat() { $xml = ''; diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata.xml new file mode 100644 index 0000000000000..69eb0035958e6 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute.xml new file mode 100644 index 0000000000000..12a9389e3d238 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_merged.xml new file mode 100644 index 0000000000000..6e95d843e34ba --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_merged.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_new.xml new file mode 100644 index 0000000000000..b905781a9fe50 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_new.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_merged.xml new file mode 100644 index 0000000000000..b905781a9fe50 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_merged.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_new.xml new file mode 100644 index 0000000000000..b905781a9fe50 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_new.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text.xml new file mode 100644 index 0000000000000..69eb0035958e6 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_merged.xml new file mode 100644 index 0000000000000..3e37e67ffcf35 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_merged.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_new.xml new file mode 100644 index 0000000000000..3e37e67ffcf35 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_new.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata.xml new file mode 100644 index 0000000000000..f65a21e122394 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html.xml new file mode 100644 index 0000000000000..15294f46445ec --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html.xml @@ -0,0 +1,12 @@ + + + + + Phrase]]> + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_merged.xml new file mode 100644 index 0000000000000..709d921f737e4 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_merged.xml @@ -0,0 +1,12 @@ + + + + + Other
Phrase]]>
+
+
diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_new.xml new file mode 100644 index 0000000000000..709d921f737e4 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_new.xml @@ -0,0 +1,12 @@ + + + + + Other
Phrase]]>
+
+
diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_merged.xml new file mode 100644 index 0000000000000..e6d2d809d7f7f --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_merged.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_new.xml new file mode 100644 index 0000000000000..e6d2d809d7f7f --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_new.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text.xml new file mode 100644 index 0000000000000..f65a21e122394 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_merged.xml new file mode 100644 index 0000000000000..3e37e67ffcf35 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_merged.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_new.xml new file mode 100644 index 0000000000000..3e37e67ffcf35 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_new.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata.xml new file mode 100644 index 0000000000000..6807872aa3d3a --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata.xml @@ -0,0 +1,12 @@ + + + + + Some Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_merged.xml new file mode 100644 index 0000000000000..b905781a9fe50 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_merged.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_new.xml new file mode 100644 index 0000000000000..b905781a9fe50 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_new.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd b/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd index 1f635b7081e05..701a2eb18c2a1 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd @@ -21,6 +21,7 @@ +
diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index e629a41056e60..fac588f1fbc1e 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Console; use Magento\Framework\App\Bootstrap; @@ -22,9 +24,10 @@ /** * Magento 2 CLI Application. + * * This is the hood for all command line tools supported by Magento. * - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Cli extends Console\Application @@ -61,11 +64,11 @@ class Cli extends Console\Application /** * @param string $name the application name * @param string $version the application version - * @SuppressWarnings(PHPMD.ExitExpression) */ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { try { + // phpcs:ignore Magento2.Security.IncludeFile $configuration = require BP . '/setup/config/application.config.php'; $bootstrapApplication = new Application(); $application = $bootstrapApplication->bootstrap($configuration); @@ -78,7 +81,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') $output->writeln( '' . $exception->getMessage() . '' ); - + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(static::RETURN_FAILURE); } @@ -93,7 +96,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') } /** - * {@inheritdoc} + * @inheritdoc * * @throws \Exception The exception in case of unexpected error */ @@ -109,7 +112,7 @@ public function doRun(Console\Input\InputInterface $input, Console\Output\Output } /** - * {@inheritdoc} + * @inheritdoc */ protected function getDefaultCommands() { @@ -217,8 +220,7 @@ protected function getVendorCommands($objectManager) } /** - * Provides updated configuration in - * accordance to document root settings. + * Provides updated configuration in accordance to document root settings. * * @param array $config * @return array diff --git a/lib/internal/Magento/Framework/Crontab/CrontabManager.php b/lib/internal/Magento/Framework/Crontab/CrontabManager.php index 6049b7fba6d44..da81540faf477 100644 --- a/lib/internal/Magento/Framework/Crontab/CrontabManager.php +++ b/lib/internal/Magento/Framework/Crontab/CrontabManager.php @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ + namespace Magento\Framework\Crontab; use Magento\Framework\App\Filesystem\DirectoryList; @@ -40,31 +41,35 @@ public function __construct( } /** + * Build tasks block start text. + * * @return string */ private function getTasksBlockStart() { $tasksBlockStart = self::TASKS_BLOCK_START; if (defined('BP')) { - $tasksBlockStart .= ' ' . md5(BP); + $tasksBlockStart .= ' ' . hash("sha256", BP); } return $tasksBlockStart; } /** + * Build tasks block end text. + * * @return string */ private function getTasksBlockEnd() { $tasksBlockEnd = self::TASKS_BLOCK_END; if (defined('BP')) { - $tasksBlockEnd .= ' ' . md5(BP); + $tasksBlockEnd .= ' ' . hash("sha256", BP); } return $tasksBlockEnd; } /** - * {@inheritdoc} + * @inheritdoc */ public function getTasks() { @@ -82,7 +87,7 @@ public function getTasks() } /** - * {@inheritdoc} + * @inheritdoc */ public function saveTasks(array $tasks) { @@ -118,8 +123,7 @@ public function saveTasks(array $tasks) } /** - * {@inheritdoc} - * @throws LocalizedException + * @inheritdoc */ public function removeTasks() { @@ -182,7 +186,7 @@ private function cleanMagentoSection($content) private function getCrontabContent() { try { - $content = (string)$this->shell->execute('crontab -l'); + $content = (string)$this->shell->execute('crontab -l 2>/dev/null'); } catch (LocalizedException $e) { return ''; } @@ -203,6 +207,7 @@ private function save($content) try { $this->shell->execute('echo "' . $content . '" | crontab -'); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (LocalizedException $e) { throw new LocalizedException( new Phrase('Error during saving of crontab: %1', [$e->getPrevious()->getMessage()]), diff --git a/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php b/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php index b160eb0a7f95e..f6c863d9d9fad 100644 --- a/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php +++ b/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ + namespace Magento\Framework\Crontab\Test\Unit; use Magento\Framework\Crontab\CrontabManager; @@ -16,6 +17,9 @@ use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\DriverPool; +/** + * Tests crontab manager functionality. + */ class CrontabManagerTest extends \PHPUnit\Framework\TestCase { /** @@ -58,7 +62,7 @@ public function testGetTasksNoCrontab() $this->shellMock->expects($this->once()) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willThrowException($localizedException); $this->assertEquals([], $this->crontabManager->getTasks()); @@ -74,7 +78,7 @@ public function testGetTasks($content, $tasks) { $this->shellMock->expects($this->once()) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn($content); $this->assertEquals($tasks, $this->crontabManager->getTasks()); @@ -88,17 +92,17 @@ public function getTasksDataProvider() return [ [ 'content' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'tasks' => ['* * * * * /bin/php /var/www/magento/bin/magento cron:run'], ], [ 'content' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento setup:cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'tasks' => [ '* * * * * /bin/php /var/www/magento/bin/magento cron:run', '* * * * * /bin/php /var/www/magento/bin/magento setup:cron:run', @@ -127,7 +131,7 @@ public function testRemoveTasksWithException() $this->shellMock->expects($this->at(0)) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn(''); $this->shellMock->expects($this->at(1)) @@ -148,7 +152,7 @@ public function testRemoveTasks($contentBefore, $contentAfter) { $this->shellMock->expects($this->at(0)) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn($contentBefore); $this->shellMock->expects($this->at(1)) @@ -166,17 +170,17 @@ public function removeTasksDataProvider() return [ [ 'contentBefore' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'contentAfter' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL ], [ 'contentBefore' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento setup:cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'contentAfter' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL ], [ @@ -276,7 +280,7 @@ public function testSaveTasks($tasks, $content, $contentToSave) $this->shellMock->expects($this->at(0)) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn($content); $this->shellMock->expects($this->at(1)) @@ -292,9 +296,9 @@ public function testSaveTasks($tasks, $content, $contentToSave) public function saveTasksDataProvider() { $content = '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL; + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL; return [ [ @@ -303,9 +307,9 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' run.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -313,9 +317,9 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '1 2 3 4 5 ' . PHP_BINARY . ' run.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -323,10 +327,10 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php >>' . ' /var/www/magento2/var/log/cron.log' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -334,10 +338,10 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php' . ' %% cron:run | grep -v \"Ran \'jobs\' by schedule\"' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -345,10 +349,10 @@ public function saveTasksDataProvider() ], 'content' => '* * * * * /bin/php /var/www/cron.php', 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php' . ' %% cron:run | grep -v \"Ran \'jobs\' by schedule\"' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], ]; } diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index 5c9bc9c2fb2d7..f654fd263f605 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\DB\Adapter; use Magento\Framework\DB\Ddl\Table; @@ -365,6 +366,7 @@ public function getIndexList($tableName, $schemaName = null); /** * Add new Foreign Key to table + * * If Foreign Key with same name is exist - it will be deleted * * @param string $fkName @@ -373,7 +375,6 @@ public function getIndexList($tableName, $schemaName = null); * @param string $refTableName * @param string $refColumnName * @param string $onDelete - * @param string $onUpdate * @param boolean $purge trying remove invalid data * @param string $schemaName * @param string $refSchemaName @@ -484,6 +485,7 @@ public function insert($table, array $bind); /** * Inserts a table row with specified data + * * Special for Zero values to identity column * * @param string $table @@ -502,9 +504,9 @@ public function insertForce($table, array $bind); * If the $where parameter is an array of multiple clauses, they will be joined by AND, with each clause wrapped in * parenthesis. If you wish to use an OR, you must give a single clause that is an instance of {@see Zend_Db_Expr} * - * @param mixed $table The table to update. - * @param array $bind Column-value pairs. - * @param mixed $where UPDATE WHERE clause(s). + * @param mixed $table The table to update. + * @param array $bind Column-value pairs. + * @param mixed $where UPDATE WHERE clause(s). * @return int The number of affected rows. */ public function update($table, array $bind, $where = ''); @@ -512,8 +514,8 @@ public function update($table, array $bind, $where = ''); /** * Deletes table rows based on a WHERE clause. * - * @param mixed $table The table to update. - * @param mixed $where DELETE WHERE clause(s). + * @param mixed $table The table to update. + * @param mixed $where DELETE WHERE clause(s). * @return int The number of affected rows. */ public function delete($table, $where = ''); @@ -521,31 +523,33 @@ public function delete($table, $where = ''); /** * Prepares and executes an SQL statement with bound data. * - * @param mixed $sql The SQL statement with placeholders. + * @param mixed $sql The SQL statement with placeholders. * May be a string or \Magento\Framework\DB\Select. - * @param mixed $bind An array of data or data itself to bind to the placeholders. + * @param mixed $bind An array of data or data itself to bind to the placeholders. * @return \Zend_Db_Statement_Interface */ public function query($sql, $bind = []); /** * Fetches all SQL result rows as a sequential array. + * * Uses the current fetchMode for the adapter. * - * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. - * @param mixed $bind Data to bind into SELECT placeholders. - * @param mixed $fetchMode Override current fetch mode. + * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @param mixed $fetchMode Override current fetch mode. * @return array */ public function fetchAll($sql, $bind = [], $fetchMode = null); /** * Fetches the first row of the SQL result. + * * Uses the current fetchMode for the adapter. * * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. * @param mixed $bind Data to bind into SELECT placeholders. - * @param mixed $fetchMode Override current fetch mode. + * @param mixed $fetchMode Override current fetch mode. * @return array */ public function fetchRow($sql, $bind = [], $fetchMode = null); @@ -622,9 +626,9 @@ public function quote($value, $type = null); * // $safe = "WHERE date < '2005-01-02'" * * - * @param string $text The text with a placeholder. - * @param mixed $value The value to quote. - * @param string $type OPTIONAL SQL datatype + * @param string $text The text with a placeholder. + * @param mixed $value The value to quote. + * @param string $type OPTIONAL SQL datatype * @param integer $count OPTIONAL count of placeholders to replace * @return string An SQL-safe quoted value placed into the original text. */ @@ -633,7 +637,7 @@ public function quoteInto($text, $value, $type = null, $count = null); /** * Quotes an identifier. * - * Accepts a string representing a qualified indentifier. For Example: + * Accepts a string representing a qualified identifier. For Example: * * $adapter->quoteIdentifier('myschema.mytable') * @@ -721,7 +725,8 @@ public function disallowDdlCache(); /** * Reset cached DDL data from cache - * if table name is null - reset all cached DDL data + * + * If table name is null - reset all cached DDL data * * @param string $tableName * @param string $schemaName OPTIONAL @@ -741,6 +746,7 @@ public function saveDdlCache($tableCacheKey, $ddlType, $data); /** * Load DDL data from cache + * * Return false if cache does not exists * * @param string $tableCacheKey the table cache key @@ -784,6 +790,7 @@ public function prepareSqlCondition($fieldName, $condition); /** * Prepare value for save in column + * * Return converted to column data type value * * @param array $column the column describe array @@ -813,6 +820,7 @@ public function getIfNullSql($expression, $value = 0); /** * Generate fragment of SQL, that combine together (concatenate) the results from data array + * * All arguments in data must be quoted * * @param array $data @@ -823,6 +831,7 @@ public function getConcatSql(array $data, $separator = null); /** * Generate fragment of SQL that returns length of character string + * * The string argument must be quoted * * @param string $string @@ -931,6 +940,7 @@ public function getDateExtractSql($date, $unit); /** * Retrieve valid table name + * * Check table name length and allowed symbols * * @param string $tableName @@ -950,6 +960,7 @@ public function getTriggerName($tableName, $time, $event); /** * Retrieve valid index name + * * Check index name length and allowed symbols * * @param string $tableName @@ -961,6 +972,7 @@ public function getIndexName($tableName, $fields, $indexType = ''); /** * Retrieve valid foreign key name + * * Check foreign key name length and allowed symbols * * @param string $priTableName @@ -1047,6 +1059,7 @@ public function supportStraightJoin(); /** * Adds order by random to select object + * * Possible using integer field for optimization * * @param \Magento\Framework\DB\Select $select @@ -1074,6 +1087,7 @@ public function getPrimaryKeyName($tableName, $schemaName = null); /** * Converts fetched blob into raw binary PHP data. + * * Some DB drivers return blobs as hex-coded strings, so we need to process them. * * @param mixed $value @@ -1114,6 +1128,8 @@ public function dropTrigger($triggerName, $schemaName = null); public function getTables($likeCondition = null); /** + * Generates case SQL fragment + * * Generate fragment of SQL, that check value against multiple condition cases * and return different result depends on them * diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 86266ec23fe47..edec7e135ae9f 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -2154,7 +2154,6 @@ public function createTable(Table $table) */ public function createTemporaryTable(\Magento\Framework\DB\Ddl\Table $table) { - $columns = $table->getColumns(); $sqlFragment = array_merge( $this->_getColumnsDefinition($table), $this->_getIndexesDefinition($table), @@ -2915,6 +2914,7 @@ public function endSetup() * - array("gteq" => $greaterOrEqualValue) * - array("lteq" => $lessOrEqualValue) * - array("finset" => $valueInSet) + * - array("nfinset" => $valueNotInSet) * - array("regexp" => $regularExpression) * - array("seq" => $stringValue) * - array("sneq" => $stringValue) @@ -2944,6 +2944,7 @@ public function prepareSqlCondition($fieldName, $condition) 'gteq' => "{{fieldName}} >= ?", 'lteq' => "{{fieldName}} <= ?", 'finset' => "FIND_IN_SET(?, {{fieldName}})", + 'nfinset' => "NOT FIND_IN_SET(?, {{fieldName}})", 'regexp' => "{{fieldName}} REGEXP ?", 'from' => "{{fieldName}} >= ?", 'to' => "{{fieldName}} <= ?", @@ -2965,7 +2966,7 @@ public function prepareSqlCondition($fieldName, $condition) if (isset($condition['to'])) { $query .= empty($query) ? '' : ' AND '; $to = $this->_prepareSqlDateCondition($condition, 'to'); - $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); + $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); } } elseif (array_key_exists($key, $conditionKeyMap)) { $value = $condition[$key]; diff --git a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php index 3ce78177d875f..f1d093b7deafa 100644 --- a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php +++ b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php @@ -22,18 +22,25 @@ class UnionExpression extends Expression */ protected $type; + /** + * @var string + */ + protected $pattern; + /** * @param Select[] $parts - * @param string $type + * @param string $type (optional) + * @param string $pattern (optional) */ - public function __construct(array $parts, $type = Select::SQL_UNION) + public function __construct(array $parts, $type = Select::SQL_UNION, $pattern = '') { $this->parts = $parts; $this->type = $type; + $this->pattern = $pattern; } /** - * @return string + * @inheritdoc */ public function __toString() { @@ -45,6 +52,10 @@ public function __toString() $parts[] = $part; } } - return implode($parts, $this->type); + $sql = implode($parts, $this->type); + if ($this->pattern) { + return sprintf($this->pattern, $sql); + } + return $sql; } } diff --git a/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php index 7b8314a76f32e..d24bc5fef6ef6 100644 --- a/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php @@ -3,21 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Framework\DB\Statement\Pdo; + +use Magento\Framework\DB\Statement\Parameter; /** * Mysql DB Statement * * @author Magento Core Team */ -namespace Magento\Framework\DB\Statement\Pdo; - -use Magento\Framework\DB\Statement\Parameter; - class Mysql extends \Zend_Db_Statement_Pdo { + /** - * Executes statement with binding values to it. - * Allows transferring specific options to DB driver. + * Executes statement with binding values to it. Allows transferring specific options to DB driver. * * @param array $params Array of values to bind to parameter placeholders. * @return bool @@ -61,11 +60,9 @@ public function _executeWithBinding(array $params) $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); } - try { + return $this->tryExecute(function () use ($statement) { return $statement->execute(); - } catch (\PDOException $e) { - throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); - } + }); } /** @@ -90,7 +87,29 @@ public function _execute(array $params = null) if ($specialExecute) { return $this->_executeWithBinding($params); } else { - return parent::_execute($params); + return $this->tryExecute(function () use ($params) { + return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); + }); + } + } + + /** + * Executes query and avoid warnings. + * + * @param callable $callback + * @return bool + * @throws \Zend_Db_Statement_Exception + */ + private function tryExecute($callback) + { + $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 + try { + return $callback(); + } catch (\PDOException $e) { + $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); + throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); + } finally { + error_reporting($previousLevel); } } } diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php new file mode 100644 index 0000000000000..714dfe6bb1059 --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php @@ -0,0 +1,154 @@ +adapterMock = $this->getMockForAbstractClass( + \Zend_Db_Adapter_Abstract::class, + [], + '', + false, + true, + true, + ['getConnection', 'getProfiler'] + ); + $this->pdoMock = $this->createMock(\PDO::class); + $this->adapterMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->pdoMock); + $this->zendDbProfilerMock = $this->createMock(\Zend_Db_Profiler::class); + $this->adapterMock->expects($this->once()) + ->method('getProfiler') + ->willReturn($this->zendDbProfilerMock); + $this->pdoStatementMock = $this->createMock(\PDOStatement::class); + } + + public function testExecuteWithoutParams() + { + $query = 'SET @a=1;'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->once()) + ->method('execute'); + (new Mysql($this->adapterMock, $query))->_execute(); + } + + public function testExecuteWhenThrowPDOException() + { + $this->expectException(\Zend_Db_Statement_Exception::class); + $this->expectExceptionMessage('test message, query was:'); + $errorReporting = error_reporting(); + $query = 'SET @a=1;'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->once()) + ->method('execute') + ->willThrowException(new \PDOException('test message')); + + $this->assertEquals($errorReporting, error_reporting(), 'Error report level was\'t restored'); + + (new Mysql($this->adapterMock, $query))->_execute(); + } + + public function testExecuteWhenParamsAsPrimitives() + { + $params = [':param1' => 'value1', ':param2' => 'value2']; + $query = 'UPDATE `some_table1` SET `col1`=\'val1\' WHERE `param1`=\':param1\' AND `param2`=\':param2\';'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->never()) + ->method('bindParam'); + $this->pdoStatementMock->expects($this->once()) + ->method('execute') + ->with($params); + + (new Mysql($this->adapterMock, $query))->_execute($params); + } + + public function testExecuteWhenParamsAsParameterObject() + { + $param1 = $this->createMock(Parameter::class); + $param1Value = 'SomeValue'; + $param1DataType = 'dataType'; + $param1Length = '9'; + $param1DriverOptions = 'some driver options'; + $param1->expects($this->once()) + ->method('getIsBlob') + ->willReturn(false); + $param1->expects($this->once()) + ->method('getDataType') + ->willReturn($param1DataType); + $param1->expects($this->once()) + ->method('getLength') + ->willReturn($param1Length); + $param1->expects($this->once()) + ->method('getDriverOptions') + ->willReturn($param1DriverOptions); + $param1->expects($this->once()) + ->method('getValue') + ->willReturn($param1Value); + $params = [ + ':param1' => $param1, + ':param2' => 'value2', + ]; + $query = 'UPDATE `some_table1` SET `col1`=\'val1\' WHERE `param1`=\':param1\' AND `param2`=\':param2\';'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->exactly(2)) + ->method('bindParam') + ->withConsecutive( + [':param1', $param1Value, $param1DataType, $param1Length, $param1DriverOptions], + [':param2', 'value2', \PDO::PARAM_STR, null, null] + ); + $this->pdoStatementMock->expects($this->once()) + ->method('execute'); + + (new Mysql($this->adapterMock, $query))->_execute($params); + } +} diff --git a/lib/internal/Magento/Framework/DB/Tree.php b/lib/internal/Magento/Framework/DB/Tree.php index 9890c6ef0d240..1aeaf122131f6 100644 --- a/lib/internal/Magento/Framework/DB/Tree.php +++ b/lib/internal/Magento/Framework/DB/Tree.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\DB; @@ -15,6 +16,7 @@ * Magento Library * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:ignoreFile * * @deprecated Not used anymore. */ @@ -300,7 +302,6 @@ public function getNodeInfo($nodeId) * @param string|int $nodeId * @param array $data * @return false|string - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ @@ -477,7 +478,6 @@ public function removeNode($nodeId) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ @@ -814,7 +814,6 @@ public function moveNode($eId, $pId, $aId = 0) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ @@ -1041,7 +1040,6 @@ protected function _addExtTablesToSelect(Select &$select) * @param int $startLevel * @param int $endLevel * @return NodeSet - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 9c789e81913c4..128d3d8e9fd3d 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Data; use Magento\Framework\Data\Collection\EntityFactoryInterface; @@ -391,7 +392,7 @@ public function getItemByColumnValue($column, $value) /** * Adding item to item array * - * @param \Magento\Framework\DataObject $item + * @param \Magento\Framework\DataObject $item * @return $this * @throws \Exception */ @@ -401,6 +402,7 @@ public function addItem(\Magento\Framework\DataObject $item) if ($itemId !== null) { if (isset($this->_items[$itemId])) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'Item (' . get_class($item) . ') with the same ID "' . $item->getId() . '" already exists.' ); @@ -452,7 +454,7 @@ public function getAllIds() /** * Remove item from collection by item key * - * @param mixed $key + * @param mixed $key * @return $this */ public function removeItemByKey($key) @@ -483,6 +485,7 @@ public function clear() { $this->_setIsLoaded(false); $this->_items = []; + $this->_totalRecords = null; return $this; } @@ -539,8 +542,8 @@ public function each($objMethod, $args = []) /** * Setting data for all collection items * - * @param mixed $key - * @param mixed $value + * @param mixed $key + * @param mixed $value * @return $this */ public function setDataToAll($key, $value = null) @@ -560,7 +563,7 @@ public function setDataToAll($key, $value = null) /** * Set current page * - * @param int $page + * @param int $page * @return $this */ public function setCurPage($page) @@ -572,7 +575,7 @@ public function setCurPage($page) /** * Set collection page size * - * @param int $size + * @param int $size * @return $this */ public function setPageSize($size) @@ -584,8 +587,8 @@ public function setPageSize($size) /** * Set select order * - * @param string $field - * @param string $direction + * @param string $field + * @param string $direction * @return $this */ public function setOrder($field, $direction = self::SORT_ORDER_DESC) @@ -597,7 +600,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) /** * Set collection item class name * - * @param string $className + * @param string $className * @return $this * @throws \InvalidArgumentException */ diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 330ff4e975e8a..b2bd352ea279c 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Data\Collection; use Magento\Framework\Data\Collection; @@ -27,63 +29,63 @@ class Filesystem extends \Magento\Framework\Data\Collection { /** - * Target directory + * Target directory. * * @var string */ protected $_targetDirs = []; /** - * Whether to collect files + * Whether to collect files. * * @var bool */ protected $_collectFiles = true; /** - * Whether to collect directories before files + * Whether to collect directories before files. * * @var bool */ protected $_dirsFirst = true; /** - * Whether to collect recursively + * Whether to collect recursively. * * @var bool */ protected $_collectRecursively = true; /** - * Whether to collect dirs + * Whether to collect dirs. * * @var bool */ protected $_collectDirs = false; /** - * \Directory names regex pre-filter + * \Directory names regex pre-filter. * * @var string */ protected $_allowedDirsMask = '/^[a-z0-9\.\-\_]+$/i'; /** - * Filenames regex pre-filter + * Filenames regex pre-filter. * * @var string */ protected $_allowedFilesMask = '/^[a-z0-9\.\-\_]+\.[a-z0-9]+$/i'; /** - * Disallowed filenames regex pre-filter match for better versatility + * Disallowed filenames regex pre-filter match for better versatility. * * @var string */ protected $_disallowedFilesMask = ''; /** - * Filter rendering helper variable + * Filter rendering helper variable. * * @var int * @see Collection::$_filter @@ -92,7 +94,7 @@ class Filesystem extends \Magento\Framework\Data\Collection private $_filterIncrement = 0; /** - * Filter rendering helper variable + * Filter rendering helper variable. * * @var array * @see Collection::$_filter @@ -101,7 +103,7 @@ class Filesystem extends \Magento\Framework\Data\Collection private $_filterBrackets = []; /** - * Filter rendering helper variable + * Filter rendering helper variable. * * @var string * @see Collection::$_filter @@ -110,22 +112,21 @@ class Filesystem extends \Magento\Framework\Data\Collection private $_filterEvalRendered = ''; /** - * Collecting items helper variable + * Collecting items helper variable. * * @var array */ protected $_collectedDirs = []; /** - * Collecting items helper variable + * Collecting items helper variable. * * @var array */ protected $_collectedFiles = []; /** - * Allowed dirs mask setter - * Set empty to not filter + * Allowed dirs mask setter. Set empty to not filter. * * @param string $regex * @return $this @@ -137,8 +138,7 @@ public function setDirsFilter($regex) } /** - * Allowed files mask setter - * Set empty to not filter + * Allowed files mask setter. Set empty to not filter. * * @param string $regex * @return $this @@ -150,8 +150,7 @@ public function setFilesFilter($regex) } /** - * Disallowed files mask setter - * Set empty value to not use this filter + * Disallowed files mask setter. Set empty value to not use this filter. * * @param string $regex * @return $this @@ -163,7 +162,7 @@ public function setDisallowedFilesFilter($regex) } /** - * Set whether to collect dirs + * Set whether to collect dirs. * * @param bool $value * @return $this @@ -175,7 +174,7 @@ public function setCollectDirs($value) } /** - * Set whether to collect files + * Set whether to collect files. * * @param bool $value * @return $this @@ -187,7 +186,7 @@ public function setCollectFiles($value) } /** - * Set whether to collect recursively + * Set whether to collect recursively. * * @param bool $value * @return $this @@ -199,7 +198,7 @@ public function setCollectRecursively($value) } /** - * Target directory setter. Adds directory to be scanned + * Target directory setter. Adds directory to be scanned. * * @param string $value * @return $this @@ -209,6 +208,7 @@ public function addTargetDir($value) { $value = (string)$value; if (!is_dir($value)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Unable to set target directory.'); } $this->_targetDirs[$value] = $value; @@ -216,8 +216,7 @@ public function addTargetDir($value) } /** - * Set whether to collect directories before files - * Works *before* sorting. + * Set whether to collect directories before files. Works *before* sorting. * * @param bool $value * @return $this @@ -229,7 +228,7 @@ public function setDirsFirst($value) } /** - * Get files from specified directory recursively (if needed) + * Get files from specified directory recursively (if needed). * * @param string|array $dir * @return void @@ -281,7 +280,7 @@ protected function _collectRecursive($dir) } /** - * Lauch data collecting + * Launch data collecting. * * @param bool $printQuery * @param bool $logQuery @@ -295,6 +294,7 @@ public function loadData($printQuery = false, $logQuery = false) return $this; } if (empty($this->_targetDirs)) { + // phpcs:disable Magento2.Exceptions.DirectThrow throw new \Exception('Please specify at least one target directory.'); } @@ -364,25 +364,22 @@ private function _generateAndFilterAndSort($attributeName) } /** - * Callback for sorting items - * Currently supports only sorting by one column + * Callback for sorting items. Currently supports only sorting by one column. * * @param array $a * @param array $b - * @return int|void + * @return int */ protected function _usort($a, $b) { foreach ($this->_orders as $key => $direction) { $result = $a[$key] > $b[$key] ? 1 : ($a[$key] < $b[$key] ? -1 : 0); return self::SORT_ORDER_ASC === strtoupper($direction) ? $result : -$result; - break; } } /** - * Set select order - * Currently supports only sorting by one column + * Set select order. Currently supports only sorting by one column. * * @param string $field * @param string $direction @@ -395,7 +392,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) } /** - * Generate item row basing on the filename + * Generate item row basing on the filename. * * @param string $filename * @return array @@ -433,13 +430,11 @@ public function addCallbackFilter($field, $value, $type, $callback, $isInverted } /** - * The filters renderer and caller - * Applies to each row, renders once. + * The filters renderer and caller. Applies to each row, renders once. * * @param array $row * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.EvalExpression) */ protected function _filterRow($row) { @@ -469,14 +464,14 @@ protected function _filterRow($row) } $result = false; if ($this->_filterEvalRendered) { + // phpcs:ignore Squiz.PHP.Eval eval('$result = ' . $this->_filterEvalRendered . ';'); } return $result; } /** - * Invokes specified callback - * Skips, if there is no filtered key in the row + * Invokes specified callback. Skips, if there is no filtered key in the row. * * @param callback $callback * @param array $callbackParams @@ -493,7 +488,7 @@ protected function _invokeFilter($callback, $callbackParams) } /** - * Fancy field filter + * Fancy field filter. * * @param string $field * @param mixed $cond @@ -626,7 +621,7 @@ public function addFieldToFilter($field, $cond, $type = 'and') } /** - * Prepare a bracket into filters + * Prepare a bracket into filters. * * @param string $bracket * @param bool $isAnd @@ -643,7 +638,7 @@ protected function _addFilterBracket($bracket = '(', $isAnd = true) } /** - * Render condition sign before element, if required + * Render condition sign before element, if required. * * @param int $increment * @param bool $isAnd @@ -666,7 +661,8 @@ protected function _renderConditionBeforeFilterElement($increment, $isAnd) } /** - * Does nothing. Intentionally disabled parent method + * Does nothing. Intentionally disabled parent method. + * * @param string $field * @param string $value * @param string $type @@ -679,7 +675,7 @@ public function addFilter($field, $value, $type = 'and') } /** - * Get all ids of collected items + * Get all ids of collected items. * * @return array */ @@ -689,7 +685,7 @@ public function getAllIds() } /** - * Callback method for 'like' fancy filter + * Callback method for 'like' fancy filter. * * @param string $field * @param mixed $filterValue @@ -700,6 +696,9 @@ public function getAllIds() */ public function filterCallbackLike($field, $filterValue, $row) { + // Forced to do this in order to keep backward compatibility for @api class. + // Strict typing must be added to this method next major release. + $filterValue = (string)$filterValue; $filterValue = trim(stripslashes($filterValue), '\''); $filterValue = trim($filterValue, '%'); $filterValueRegex = '(.*?)' . preg_quote($filterValue, '/') . '(.*?)'; @@ -708,7 +707,7 @@ public function filterCallbackLike($field, $filterValue, $row) } /** - * Callback method for 'eq' fancy filter + * Callback method for 'eq' fancy filter. * * @param string $field * @param mixed $filterValue @@ -723,7 +722,7 @@ public function filterCallbackEq($field, $filterValue, $row) } /** - * Callback method for 'in' fancy filter + * Callback method for 'in' fancy filter. * * @param string $field * @param mixed $filterValue @@ -738,7 +737,7 @@ public function filterCallbackInArray($field, $filterValue, $row) } /** - * Callback method for 'isnull' fancy filter + * Callback method for 'isnull' fancy filter. * * @param string $field * @param mixed $filterValue @@ -754,7 +753,7 @@ public function filterCallbackIsNull($field, $filterValue, $row) } /** - * Callback method for 'moreq' fancy filter + * Callback method for 'moreq' fancy filter. * * @param string $field * @param mixed $filterValue @@ -769,7 +768,7 @@ public function filterCallbackIsMoreThan($field, $filterValue, $row) } /** - * Callback method for 'lteq' fancy filter + * Callback method for 'lteq' fancy filter. * * @param string $field * @param mixed $filterValue diff --git a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php index a8451e43ade20..14f4df7208b04 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php @@ -170,7 +170,11 @@ public function setId($id) */ public function getHtmlId() { - return $this->getForm()->getHtmlIdPrefix() . $this->getData('html_id') . $this->getForm()->getHtmlIdSuffix(); + return $this->_escaper->escapeHtml( + $this->getForm()->getHtmlIdPrefix() . + $this->getData('html_id') . + $this->getForm()->getHtmlIdSuffix() + ); } /** @@ -180,7 +184,7 @@ public function getHtmlId() */ public function getName() { - $name = $this->getData('name'); + $name = $this->_escaper->escapeHtml($this->getData('name')); if ($suffix = $this->getForm()->getFieldNameSuffix()) { $name = $this->getForm()->addSuffixToName($name, $suffix); } @@ -201,6 +205,8 @@ public function setType($type) } /** + * Set form. + * * @param AbstractForm $form * @return $this */ @@ -238,6 +244,7 @@ public function getHtmlAttributes() 'onchange', 'disabled', 'readonly', + 'autocomplete', 'tabindex', 'placeholder', 'data-form-part', @@ -326,6 +333,8 @@ public function getRenderer() } /** + * Get Ui Id. + * * @param null|string $suffix * @return string */ @@ -334,7 +343,7 @@ protected function _getUiId($suffix = null) if ($this->_renderer instanceof \Magento\Framework\View\Element\AbstractBlock) { return $this->_renderer->getUiId($this->getType(), $this->getName(), $suffix); } else { - return ' data-ui-id="form-element-' . $this->getName() . ($suffix ?: '') . '"'; + return ' data-ui-id="form-element-' . $this->_escaper->escapeHtml($this->getName()) . ($suffix ?: '') . '"'; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Date.php b/lib/internal/Magento/Framework/Data/Form/Element/Date.php index c519ecfed4c82..6e4e97dbac79d 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Date.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Date.php @@ -53,6 +53,19 @@ public function __construct( } } + /** + * Check if a string is a date value + * + * @param string $value + * @return bool + */ + private function isDate(string $value): bool + { + $date = date_parse($value); + + return !empty($date['year']) && !empty($date['month']) && !empty($date['day']); + } + /** * If script executes on x64 system, converts large numeric values to timestamp limit * @@ -85,13 +98,13 @@ public function setValue($value) $this->_value = $value; return $this; } - try { if (preg_match('/^[0-9]+$/', $value)) { $this->_value = (new \DateTime())->setTimestamp($this->_toTimestamp($value)); + } elseif (is_string($value) && $this->isDate($value)) { + $this->_value = new \DateTime($value, new \DateTimeZone($this->localeDate->getConfigTimezone())); } else { - $this->_value = new \DateTime($value); - $this->_value->setTimezone(new \DateTimeZone($this->localeDate->getConfigTimezone())); + $this->_value = ''; } } catch (\Exception $e) { $this->_value = ''; @@ -151,7 +164,7 @@ public function getValueInstance() */ public function getElementHtml() { - $this->addClass('admin__control-text input-text'); + $this->addClass('admin__control-text input-text input-date'); $dateFormat = $this->getDateFormat() ?: $this->getFormat(); $timeFormat = $this->getTimeFormat(); if (empty($dateFormat)) { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Label.php b/lib/internal/Magento/Framework/Data/Form/Element/Label.php index 901dcb5289e8d..70b7885e7a0d0 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Label.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Label.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Data form abstract class - * - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\Phrase; + +/** + * Label form element. + */ class Label extends \Magento\Framework\Data\Form\Element\AbstractElement { /** @@ -37,8 +37,13 @@ public function __construct( public function getElementHtml() { $html = $this->getBold() ? '
' : '
'; - $html .= $this->getEscapedValue() . '
'; + if (is_string($this->getValue()) || $this->getValue() instanceof Phrase) { + $html .= $this->getEscapedValue(); + } + + $html .= '
'; $html .= $this->getAfterElementHtml(); + return $html; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php b/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php index 7cd3fb1f7fb99..1970ebeb9544e 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Form textarea element - * - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; use Magento\Framework\Escaper; +/** + * Form textarea element. + * + * @author Magento Core Team + */ class Textarea extends AbstractElement { /** @@ -64,6 +64,7 @@ public function getHtmlAttributes() 'rows', 'cols', 'readonly', + 'maxlength', 'disabled', 'onkeyup', 'tabindex', diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php index 5608959c110ff..daadeae2ac0e2 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Data\Test\Unit; +/** + * Class for Collection test. + */ class CollectionTest extends \PHPUnit\Framework\TestCase { /** @@ -12,6 +15,9 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ protected $_model; + /** + * Set up. + */ protected function setUp() { $this->_model = new \Magento\Framework\Data\Collection( @@ -19,6 +25,11 @@ protected function setUp() ); } + /** + * Test for method removeAllItems. + * + * @return void + */ public function testRemoveAllItems() { $this->_model->addItem(new \Magento\Framework\DataObject()); @@ -30,6 +41,7 @@ public function testRemoveAllItems() /** * Test loadWithFilter() + * * @return void */ public function testLoadWithFilter() @@ -42,6 +54,8 @@ public function testLoadWithFilter() } /** + * Test for method etItemObjectClass + * * @dataProvider setItemObjectClassDataProvider */ public function testSetItemObjectClass($class) @@ -51,6 +65,8 @@ public function testSetItemObjectClass($class) } /** + * Data provider. + * * @return array */ public function setItemObjectClassDataProvider() @@ -59,6 +75,8 @@ public function setItemObjectClassDataProvider() } /** + * Test for method setItemObjectClass with exception. + * * @expectedException \InvalidArgumentException * @expectedExceptionMessage Incorrect_ClassName does not extend \Magento\Framework\DataObject */ @@ -67,12 +85,22 @@ public function testSetItemObjectClassException() $this->_model->setItemObjectClass('Incorrect_ClassName'); } + /** + * Test for method addFilter. + * + * @return void + */ public function testAddFilter() { $this->_model->addFilter('field1', 'value'); $this->assertEquals('field1', $this->_model->getFilter('field1')->getData('field')); } + /** + * Test for method getFilters. + * + * @return void + */ public function testGetFilters() { $this->_model->addFilter('field1', 'value'); @@ -81,12 +109,22 @@ public function testGetFilters() $this->assertEquals('field2', $this->_model->getFilter(['field1', 'field2'])[1]->getData('field')); } + /** + * Test for method get non existion filters. + * + * @return void + */ public function testGetNonExistingFilters() { $this->assertEmpty($this->_model->getFilter([])); $this->assertEmpty($this->_model->getFilter('non_existing_filter')); } + /** + * Test for lag. + * + * @return void + */ public function testFlag() { $this->_model->setFlag('flag_name', 'flag_value'); @@ -95,12 +133,22 @@ public function testFlag() $this->assertNull($this->_model->getFlag('non_existing_flag')); } + /** + * Test for method getCurPage. + * + * @return void + */ public function testGetCurPage() { - $this->_model->setCurPage(10); + $this->_model->setCurPage(1); $this->assertEquals(1, $this->_model->getCurPage()); } + /** + * Test for method possibleFlowWithItem. + * + * @return void + */ public function testPossibleFlowWithItem() { $firstItemMock = $this->createPartialMock( @@ -168,6 +216,11 @@ public function testPossibleFlowWithItem() $this->assertEquals([], $this->_model->getItems()); } + /** + * Test for method eachCallsMethodOnEachItemWithNoArgs. + * + * @return void + */ public function testEachCallsMethodOnEachItemWithNoArgs() { for ($i = 0; $i < 3; $i++) { @@ -177,7 +230,12 @@ public function testEachCallsMethodOnEachItemWithNoArgs() } $this->_model->each('testCallback'); } - + + /** + * Test for method eachCallsMethodOnEachItemWithArgs. + * + * @return void + */ public function testEachCallsMethodOnEachItemWithArgs() { for ($i = 0; $i < 3; $i++) { @@ -188,6 +246,11 @@ public function testEachCallsMethodOnEachItemWithArgs() $this->_model->each('testCallback', ['a', 'b', 'c']); } + /** + * Test for method callsClosureWithEachItemAndNoArgs. + * + * @return void + */ public function testCallsClosureWithEachItemAndNoArgs() { for ($i = 0; $i < 3; $i++) { @@ -200,6 +263,11 @@ public function testCallsClosureWithEachItemAndNoArgs() }); } + /** + * Test for method callsClosureWithEachItemAndArgs. + * + * @return void + */ public function testCallsClosureWithEachItemAndArgs() { for ($i = 0; $i < 3; $i++) { @@ -212,6 +280,11 @@ public function testCallsClosureWithEachItemAndArgs() }, ['a', 'b', 'c']); } + /** + * Test for method callsCallableArrayWithEachItemNoArgs. + * + * @return void + */ public function testCallsCallableArrayWithEachItemNoArgs() { $mockCallbackObject = $this->getMockBuilder('DummyEachCallbackInstance') @@ -230,6 +303,11 @@ public function testCallsCallableArrayWithEachItemNoArgs() $this->_model->each([$mockCallbackObject, 'testObjCallback']); } + /** + * Test for method callsCallableArrayWithEachItemAndArgs. + * + * @return void + */ public function testCallsCallableArrayWithEachItemAndArgs() { $mockCallbackObject = $this->getMockBuilder('DummyEachCallbackInstance') diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php index e29b1dcf441e4..d9dafddc571b8 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php @@ -33,11 +33,12 @@ class AbstractElementTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $this->_collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $this->_escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->_escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_model = $this->getMockForAbstractClass( \Magento\Framework\Data\Form\Element\AbstractElement::class, @@ -195,6 +196,7 @@ public function testGetHtmlAttributes() 'onchange', 'disabled', 'readonly', + 'autocomplete', 'tabindex', 'placeholder', 'data-form-part', @@ -422,9 +424,6 @@ public function testGetHtmlContainerIdWithFieldContainerIdPrefix() */ public function testAddElementValues(array $initialData, $expectedValue) { - $this->_escaperMock->expects($this->any()) - ->method('escapeHtml') - ->will($this->returnArgument(0)); $this->_model->setValues($initialData['initial_values']); $this->_model->addElementValues($initialData['add_values'], $initialData['overwrite']); diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php index a2a40ee03b044..cf3cd0345e174 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php @@ -23,9 +23,10 @@ class LinkTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_link = new \Magento\Framework\Data\Form\Element\Link( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php index 47eae73d8cd8c..c515e0aca01df 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Data\Test\Unit\Form\Element; +use Magento\Framework\Escaper; + class MultiselectTest extends \PHPUnit\Framework\TestCase { /** @@ -15,7 +17,13 @@ class MultiselectTest extends \PHPUnit\Framework\TestCase protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_model = $testHelper->getObject(\Magento\Framework\Data\Form\Element\Editablemultiselect::class); + $escaper = new Escaper(); + $this->_model = $testHelper->getObject( + \Magento\Framework\Data\Form\Element\Editablemultiselect::class, + [ + '_escaper' => $escaper + ] + ); $this->_model->setForm(new \Magento\Framework\DataObject()); } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php index f77f4a816a1af..ad7d20fdc0acc 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php @@ -23,9 +23,10 @@ class NoteTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_model = new \Magento\Framework\Data\Form\Element\Note( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php index e99df6c4c6e6f..eec85ca35775d 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Tests for \Magento\Framework\Data\Form\Element\Textarea - */ namespace Magento\Framework\Data\Test\Unit\Form\Element; +/** + * Tests for \Magento\Framework\Data\Form\Element\Textarea class. + */ class TextareaTest extends \PHPUnit\Framework\TestCase { /** @@ -76,6 +76,7 @@ public function testGetHtmlAttributes() 'rows', 'cols', 'readonly', + 'maxlength', 'disabled', 'onkeyup', 'tabindex', diff --git a/lib/internal/Magento/Framework/DataObject.php b/lib/internal/Magento/Framework/DataObject.php index 9ff004c53bb9b..6ecbca133e22a 100644 --- a/lib/internal/Magento/Framework/DataObject.php +++ b/lib/internal/Magento/Framework/DataObject.php @@ -64,8 +64,8 @@ public function addData(array $arr) * * If $key is an array, it will overwrite all the data in the object. * - * @param string|array $key - * @param mixed $value + * @param string|array $key + * @param mixed $value * @return $this */ public function setData($key, $value = null) @@ -111,7 +111,7 @@ public function unsetData($key = null) * and retrieve corresponding member. If data is the string - it will be explode * by new line character and converted to array. * - * @param string $key + * @param string $key * @param string|int $index * @return mixed */ @@ -202,7 +202,7 @@ protected function _getData($key) */ public function setDataUsingMethod($key, $args = []) { - $method = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); + $method = 'set' . str_replace('_', '', ucwords($key, '_')); $this->{$method}($args); return $this; } @@ -216,12 +216,13 @@ public function setDataUsingMethod($key, $args = []) */ public function getDataUsingMethod($key, $args = null) { - $method = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); + $method = 'get' . str_replace('_', '', ucwords($key, '_')); return $this->{$method}($args); } /** * If $key is empty, checks whether there's any data in the object + * * Otherwise checks if the specified attribute is set. * * @param string $key @@ -272,8 +273,8 @@ public function convertToArray(array $keys = []) /** * Convert object data into XML string * - * @param array $keys array of keys that must be represented - * @param string $rootName root node name + * @param array $keys array of keys that must be represented + * @param string $rootName root node name * @param bool $addOpenTag flag that allow to add initial xml node * @param bool $addCdata flag that require wrap all values in CDATA * @return string @@ -436,7 +437,7 @@ protected function _underscore($name) * * Example: key1="value1" key2="value2" ... * - * @param array $keys array of accepted keys + * @param array $keys array of accepted keys * @param string $valueSeparator separator between key and value * @param string $fieldSeparator separator between key/value pairs * @param string $quote quoting sign diff --git a/lib/internal/Magento/Framework/DataObject/Copy.php b/lib/internal/Magento/Framework/DataObject/Copy.php index 8d8896c6cb62a..6a908ae78a343 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy.php +++ b/lib/internal/Magento/Framework/DataObject/Copy.php @@ -239,7 +239,7 @@ protected function _setFieldsetFieldValue($target, $targetCode, $value) */ protected function getAttributeValueFromExtensibleDataObject($source, $code) { - $method = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $code))); + $method = 'get' . str_replace('_', '', ucwords($code, '_')); $methodExists = method_exists($source, $method); if ($methodExists == true) { @@ -273,7 +273,7 @@ protected function getAttributeValueFromExtensibleDataObject($source, $code) */ protected function setAttributeValueFromExtensibleDataObject($target, $code, $value) { - $method = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $code))); + $method = 'set' . str_replace('_', '', ucwords($code, '_')); $methodExists = method_exists($target, $method); if ($methodExists == true) { diff --git a/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php b/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php index 9f9facf98ff84..0c56c2217669f 100644 --- a/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php +++ b/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php @@ -33,6 +33,7 @@ public function __construct( * * @param string $data * @return string string + * @throws \SodiumException */ public function encrypt(string $data): string { @@ -58,13 +59,17 @@ public function decrypt(string $data): string $nonce = mb_substr($data, 0, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, '8bit'); $payload = mb_substr($data, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, null, '8bit'); - $plainText = sodium_crypto_aead_chacha20poly1305_ietf_decrypt( - $payload, - $nonce, - $nonce, - $this->key - ); + try { + $plainText = sodium_crypto_aead_chacha20poly1305_ietf_decrypt( + $payload, + $nonce, + $nonce, + $this->key + ); + } catch (\SodiumException $e) { + $plainText = ''; + } - return $plainText; + return $plainText !== false ? $plainText : ''; } } diff --git a/lib/internal/Magento/Framework/Encryption/Encryptor.php b/lib/internal/Magento/Framework/Encryption/Encryptor.php index 676feac5ed05f..791e6d72b951f 100644 --- a/lib/internal/Magento/Framework/Encryption/Encryptor.php +++ b/lib/internal/Magento/Framework/Encryption/Encryptor.php @@ -9,6 +9,7 @@ namespace Magento\Framework\Encryption; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Encryption\Adapter\EncryptionAdapterInterface; use Magento\Framework\Encryption\Helper\Security; use Magento\Framework\Math\Random; @@ -115,20 +116,28 @@ class Encryptor implements EncryptorInterface */ private $random; + /** + * @var KeyValidator + */ + private $keyValidator; + /** * Encryptor constructor. * @param Random $random * @param DeploymentConfig $deploymentConfig + * @param KeyValidator|null $keyValidator */ public function __construct( Random $random, - DeploymentConfig $deploymentConfig + DeploymentConfig $deploymentConfig, + KeyValidator $keyValidator = null ) { $this->random = $random; // load all possible keys $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get(self::PARAM_CRYPT_KEY))); $this->keyVersion = count($this->keys) - 1; + $this->keyValidator = $keyValidator ?: ObjectManager::getInstance()->get(KeyValidator::class); } /** @@ -374,8 +383,12 @@ public function decrypt($data) */ public function validateKey($key) { - if (preg_match('/\s/s', $key)) { - throw new \Exception((string)new \Magento\Framework\Phrase('The encryption key format is invalid.')); + if (!$this->keyValidator->isValid($key)) { + throw new \Exception( + (string)new \Magento\Framework\Phrase( + 'Encryption key must be 32 character string without any white space.' + ) + ); } } diff --git a/lib/internal/Magento/Framework/Encryption/KeyValidator.php b/lib/internal/Magento/Framework/Encryption/KeyValidator.php new file mode 100644 index 0000000000000..79d592bec2a15 --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/KeyValidator.php @@ -0,0 +1,33 @@ +encrypt($decrypted); $this->assertNotEquals($encrypted, $result); @@ -40,10 +49,14 @@ public function testEncrypt(string $key, string $encrypted, string $decrypted) /** * @dataProvider getCryptData + * + * @param string $key + * @param string $encrypted + * @param string $decrypted */ - public function testDecrypt(string $key, string $encrypted, string $decrypted) + public function testDecrypt(string $key, string $encrypted, string $decrypted): void { - $crypt = new \Magento\Framework\Encryption\Adapter\SodiumChachaIetf($key); + $crypt = new SodiumChachaIetf($key); $result = $crypt->decrypt($encrypted); $this->assertEquals($decrypted, $result); diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php index 7917bd9ba83e8..8498f9a1a873f 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php @@ -32,4 +32,14 @@ 'encrypted' => 'UglO9dEgslFpwPwejJmrK89PmBicv+I1pfdaXaEI69IrETD8LpdzOLF7', 'decrypted' => 'Hello World!!!', ], + 5 => [ + 'key' => '6wRADHwwCBGgdxbcHhovGB0upmg0mbsN', + 'encrypted' => '', + 'decrypted' => '', + ], + 6 => [ + 'key' => '6wRADHwwCBGgdxbcHhovGB0upmg0mbsN', + 'encrypted' => 'bWFsZm9ybWVkLWlucHV0', + 'decrypted' => '', + ], ]; diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php index 98bb1c5676d6c..3feb4b4122843 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php @@ -8,75 +8,92 @@ namespace Magento\Framework\Encryption\Test\Unit; -use Magento\Framework\Encryption\Adapter\Mcrypt; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Encryption\Adapter\SodiumChachaIetf; -use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\Crypt; +use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\Math\Random; +use Magento\Framework\Encryption\KeyValidator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; class EncryptorTest extends \PHPUnit\Framework\TestCase { - const CRYPT_KEY_1 = 'g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'; - const CRYPT_KEY_2 = '7wEjmrliuqZQ1NQsndSa8C8WHvddeEbN'; + private const CRYPT_KEY_1 = 'g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'; + private const CRYPT_KEY_2 = '7wEjmrliuqZQ1NQsndSa8C8WHvddeEbN'; + + /** + * @var Encryptor + */ + private $encryptor; /** - * @var \Magento\Framework\Encryption\Encryptor + * @var Random | \PHPUnit_Framework_MockObject_MockObject */ - protected $_model; + private $randomGeneratorMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var KeyValidator | \PHPUnit_Framework_MockObject_MockObject */ - protected $_randomGenerator; + private $keyValidatorMock; protected function setUp() { - $this->_randomGenerator = $this->createMock(\Magento\Framework\Math\Random::class); - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $this->randomGeneratorMock = $this->createMock(Random::class); + /** @var DeploymentConfig | \PHPUnit_Framework_MockObject_MockObject $deploymentConfigMock */ + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue(self::CRYPT_KEY_1)); - $this->_model = new \Magento\Framework\Encryption\Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn(self::CRYPT_KEY_1); + $this->keyValidatorMock = $this->createMock(KeyValidator::class); + $this->encryptor = (new ObjectManager($this))->getObject( + Encryptor::class, + [ + 'random' => $this->randomGeneratorMock, + 'deploymentConfig' => $deploymentConfigMock, + 'keyValidator' => $this->keyValidatorMock + ] + ); } - public function testGetHashNoSalt() + public function testGetHashNoSalt(): void { - $this->_randomGenerator->expects($this->never())->method('getRandomString'); + $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); $expected = '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'; - $actual = $this->_model->getHash('password'); + $actual = $this->encryptor->getHash('password'); $this->assertEquals($expected, $actual); } - public function testGetHashSpecifiedSalt() + public function testGetHashSpecifiedSalt(): void { - $this->_randomGenerator->expects($this->never())->method('getRandomString'); + $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); $expected = '13601bda4ea78e55a07b98866d2be6be0744e3866f13c00c811cab608a28f322:salt:1'; - $actual = $this->_model->getHash('password', 'salt'); + $actual = $this->encryptor->getHash('password', 'salt'); $this->assertEquals($expected, $actual); } - public function testGetHashRandomSaltDefaultLength() + public function testGetHashRandomSaltDefaultLength(): void { $salt = '-----------random_salt----------'; - $this->_randomGenerator + $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') ->with(32) - ->will($this->returnValue($salt)); + ->willReturn($salt); $expected = 'a1c7fc88037b70c9be84d3ad12522c7888f647915db78f42eb572008422ba2fa:' . $salt . ':1'; - $actual = $this->_model->getHash('password', true); + $actual = $this->encryptor->getHash('password', true); $this->assertEquals($expected, $actual); } - public function testGetHashRandomSaltSpecifiedLength() + public function testGetHashRandomSaltSpecifiedLength(): void { - $this->_randomGenerator + $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') ->with(11) - ->will($this->returnValue('random_salt')); + ->willReturn('random_salt'); $expected = '4c5cab8dd00137d11258f8f87b93fd17bd94c5026fc52d3c5af911dd177a2611:random_salt:1'; - $actual = $this->_model->getHash('password', 11); + $actual = $this->encryptor->getHash('password', 11); $this->assertEquals($expected, $actual); } @@ -87,16 +104,16 @@ public function testGetHashRandomSaltSpecifiedLength() * * @dataProvider validateHashDataProvider */ - public function testValidateHash($password, $hash, $expected) + public function testValidateHash($password, $hash, $expected): void { - $actual = $this->_model->validateHash($password, $hash); + $actual = $this->encryptor->validateHash($password, $hash); $this->assertEquals($expected, $actual); } /** * @return array */ - public function validateHashDataProvider() + public function validateHashDataProvider(): array { return [ ['password', 'hash:salt:1', false], @@ -111,14 +128,14 @@ public function validateHashDataProvider() * @dataProvider encryptWithEmptyKeyDataProvider * @expectedException \SodiumException */ - public function testEncryptWithEmptyKey($key) + public function testEncryptWithEmptyKey($key): void { - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue($key)); - $model = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn($key); + $model = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); $value = 'arbitrary_string'; $this->assertEquals($value, $model->encrypt($value)); } @@ -126,7 +143,7 @@ public function testEncryptWithEmptyKey($key) /** * @return array */ - public function encryptWithEmptyKeyDataProvider() + public function encryptWithEmptyKeyDataProvider(): array { return [[null], [0], [''], ['0']]; } @@ -136,14 +153,14 @@ public function encryptWithEmptyKeyDataProvider() * * @dataProvider decryptWithEmptyKeyDataProvider */ - public function testDecryptWithEmptyKey($key) + public function testDecryptWithEmptyKey($key): void { - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue($key)); - $model = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn($key); + $model = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); $value = 'arbitrary_string'; $this->assertEquals('', $model->decrypt($value)); } @@ -151,46 +168,44 @@ public function testDecryptWithEmptyKey($key) /** * @return array */ - public function decryptWithEmptyKeyDataProvider() + public function decryptWithEmptyKeyDataProvider(): array { return [[null], [0], [''], ['0']]; } - public function testEncrypt() + public function testEncrypt(): void { // sample data to encrypt $data = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; - $actual = $this->_model->encrypt($data); + $actual = $this->encryptor->encrypt($data); // Extract the initialization vector and encrypted data - $parts = explode(':', $actual, 3); - list(, , $encryptedData) = $parts; + [, , $encryptedData] = explode(':', $actual, 3); $crypt = new SodiumChachaIetf(self::CRYPT_KEY_1); // Verify decrypted matches original data $this->assertEquals($data, $crypt->decrypt(base64_decode((string)$encryptedData))); } - public function testDecrypt() + public function testDecrypt(): void { $message = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; - $encrypted = $this->_model->encrypt($message); + $encrypted = $this->encryptor->encrypt($message); - $this->assertEquals($message, $this->_model->decrypt($encrypted)); + $this->assertEquals($message, $this->encryptor->decrypt($encrypted)); } - public function testLegacyDecrypt() + public function testLegacyDecrypt(): void { // sample data to encrypt $data = '0:2:z3a4ACpkU35W6pV692U4ueCVQP0m0v0p:' . 'DhEG8/uKGGq92ZusqrGb6X/9+2Ng0QZ9z2UZwljgJbs5/A3LaSnqcK0oI32yjHY49QJi+Z7q1EKu2yVqB8EMpA=='; - $actual = $this->_model->decrypt($data); + $actual = $this->encryptor->decrypt($data); // Extract the initialization vector and encrypted data - $parts = explode(':', $data, 4); - list(, , $iv, $encrypted) = $parts; + [, , $iv, $encrypted] = explode(':', $data, 4); // Decrypt returned data with RIJNDAEL_256 cipher, cbc mode $crypt = new Crypt(self::CRYPT_KEY_1, MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, $iv); @@ -198,20 +213,20 @@ public function testLegacyDecrypt() $this->assertEquals($encrypted, base64_encode($crypt->encrypt($actual))); } - public function testEncryptDecryptNewKeyAdded() + public function testEncryptDecryptNewKeyAdded(): void { - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->at(0)) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue(self::CRYPT_KEY_1)); + ->willReturn(self::CRYPT_KEY_1); $deploymentConfigMock->expects($this->at(1)) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue(self::CRYPT_KEY_1 . "\n" . self::CRYPT_KEY_2)); - $model1 = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn(self::CRYPT_KEY_1 . "\n" . self::CRYPT_KEY_2); + $model1 = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); // simulate an encryption key is being added - $model2 = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + $model2 = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); // sample data to encrypt $data = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; @@ -222,23 +237,25 @@ public function testEncryptDecryptNewKeyAdded() $this->assertSame($data, $decryptedData, 'Encryptor failed to decrypt data encrypted by old keys.'); } - public function testValidateKey() + public function testValidateKey(): void { - $this->_model->validateKey(self::CRYPT_KEY_1); + $this->keyValidatorMock->method('isValid')->willReturn(true); + $this->encryptor->validateKey(self::CRYPT_KEY_1); } /** * @expectedException \Exception */ - public function testValidateKeyInvalid() + public function testValidateKeyInvalid(): void { - $this->_model->validateKey('----- '); + $this->keyValidatorMock->method('isValid')->willReturn(false); + $this->encryptor->validateKey('----- '); } /** * @return array */ - public function useSpecifiedHashingAlgoDataProvider() + public function useSpecifiedHashingAlgoDataProvider(): array { return [ ['password', 'salt', Encryptor::HASH_VERSION_MD5, @@ -260,9 +277,9 @@ public function useSpecifiedHashingAlgoDataProvider() * @param $hashAlgo * @param $expected */ - public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected) + public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected): void { - $hash = $this->_model->getHash($password, $salt, $hashAlgo); + $hash = $this->encryptor->getHash($password, $salt, $hashAlgo); $this->assertEquals($expected, $hash); } } diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php new file mode 100644 index 0000000000000..85faa0aa4676f --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php @@ -0,0 +1,54 @@ +keyValidator = (new ObjectManager($this))->getObject(KeyValidator::class); + } + + /** + * @param $key + * @param bool $expected + * @dataProvider isValidDataProvider + */ + public function testIsValid($key, $expected = true) + { + $this->assertEquals($expected, $this->keyValidator->isValid($key)); + } + + public function isValidDataProvider() : array + { + return [ + '32 numbers' => ['12345678901234567890123456789012'], + '32 characters' => ['aBcdeFghIJKLMNOPQRSTUvwxYzabcdef'], + '32 special characters' => ['!@#$%^&*()_+~`:;"<>,.?/|*&^%$#@!'], + '32 combination' =>['1234eFghI1234567^&*(890123456789'], + 'empty string' => ['', false], + 'leading space' => [' 1234567890123456789012345678901', false], + 'tailing space' => ['1234567890123456789012345678901 ', false], + 'space in the middle' => ['12345678901 23456789012345678901', false], + 'tab in the middle' => ['12345678901 23456789012345678', false], + 'return in the middle' => ['12345678901 + 23456789012345678901', false], + '31 characters' => ['1234567890123456789012345678901', false], + '33 characters' => ['123456789012345678901234567890123', false], + ]; + } +} diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index 19a9c0c1788fc..c4150851ec40d 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -46,20 +46,6 @@ class Escaper */ private $escapeAsUrlAttributes = ['href']; - /** - * @param \Magento\Framework\ZendEscaper|null $escaper - * @param \Psr\Log\LoggerInterface|null $logger - */ - public function __construct( - \Magento\Framework\ZendEscaper $escaper = null, - \Psr\Log\LoggerInterface $logger = null - ) { - $this->escaper = $escaper ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\ZendEscaper::class); - $this->logger = $logger ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Psr\Log\LoggerInterface::class); - } - /** * Escape string for HTML context. * @@ -99,7 +85,7 @@ function ($errorNumber, $errorString) { ); } catch (\Exception $e) { restore_error_handler(); - $this->logger->critical($e); + $this->getLogger()->critical($e); } restore_error_handler(); @@ -229,7 +215,7 @@ private function escapeAttributeValue($name, $value) public function escapeHtmlAttr($string, $escapeSingleQuote = true) { if ($escapeSingleQuote) { - return $this->escaper->escapeHtmlAttr((string) $string); + return $this->getEscaper()->escapeHtmlAttr((string) $string); } return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8', false); } @@ -254,7 +240,7 @@ public function escapeUrl($string) */ public function encodeUrlParam($string) { - return $this->escaper->escapeUrl($string); + return $this->getEscaper()->escapeUrl($string); } /** @@ -293,7 +279,7 @@ function ($matches) { */ public function escapeCss($string) { - return $this->escaper->escapeCss($string); + return $this->getEscaper()->escapeCss($string); } /** @@ -368,6 +354,36 @@ public function escapeQuote($data, $addSlashes = false) return htmlspecialchars($data, ENT_QUOTES, null, false); } + /** + * Get escaper + * + * @return \Magento\Framework\ZendEscaper + * @deprecated 100.2.0 + */ + private function getEscaper() + { + if ($this->escaper == null) { + $this->escaper = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\ZendEscaper::class); + } + return $this->escaper; + } + + /** + * Get logger + * + * @return \Psr\Log\LoggerInterface + * @deprecated 100.2.0 + */ + private function getLogger() + { + if ($this->logger == null) { + $this->logger = \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Psr\Log\LoggerInterface::class); + } + return $this->logger; + } + /** * Filter prohibited tags. * @@ -382,7 +398,7 @@ private function filterProhibitedTags(array $allowedTags): array ); if (!empty($notAllowedTags)) { - $this->logger->critical( + $this->getLogger()->critical( 'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags) ); $allowedTags = array_diff($allowedTags, $this->notAllowedTags); diff --git a/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php b/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php index a7a387b5def81..acd0a61633557 100644 --- a/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php +++ b/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php @@ -9,7 +9,12 @@ namespace Magento\Framework\Event\Invoker; use Magento\Framework\Event\Observer; +use Psr\Log\LoggerInterface; +use Magento\Framework\App\State; +/** + * Default Invoker. + */ class InvokerDefault implements \Magento\Framework\Event\InvokerInterface { /** @@ -22,20 +27,29 @@ class InvokerDefault implements \Magento\Framework\Event\InvokerInterface /** * Application state * - * @var \Magento\Framework\App\State + * @var State */ protected $_appState; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param \Magento\Framework\Event\ObserverFactory $observerFactory - * @param \Magento\Framework\App\State $appState + * @param State $appState + * @param LoggerInterface $logger */ public function __construct( \Magento\Framework\Event\ObserverFactory $observerFactory, - \Magento\Framework\App\State $appState + State $appState, + LoggerInterface $logger = null ) { $this->_observerFactory = $observerFactory; $this->_appState = $appState; + $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** @@ -61,6 +75,8 @@ public function dispatch(array $configuration, Observer $observer) } /** + * Execute Observer. + * * @param \Magento\Framework\Event\ObserverInterface $object * @param Observer $observer * @return $this @@ -70,7 +86,7 @@ protected function _callObserverMethod($object, $observer) { if ($object instanceof \Magento\Framework\Event\ObserverInterface) { $object->execute($observer); - } elseif ($this->_appState->getMode() == \Magento\Framework\App\State::MODE_DEVELOPER) { + } elseif ($this->_appState->getMode() == State::MODE_DEVELOPER) { throw new \LogicException( sprintf( 'Observer "%s" must implement interface "%s"', @@ -78,6 +94,12 @@ protected function _callObserverMethod($object, $observer) \Magento\Framework\Event\ObserverInterface::class ) ); + } else { + $this->logger->warning(sprintf( + 'Observer "%s" must implement interface "%s"', + get_class($object), + \Magento\Framework\Event\ObserverInterface::class + )); } return $this; } diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php b/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php index 37f650dbef6a0..e6ec123823854 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Event\Test\Unit\Invoker; +/** + * Test for Magento\Framework\Event\Invoker\InvokerDefault. + */ class InvokerDefaultTest extends \PHPUnit\Framework\TestCase { /** @@ -32,6 +35,11 @@ class InvokerDefaultTest extends \PHPUnit\Framework\TestCase */ protected $_invokerDefault; + /** + * @var |Psr\Log|LoggerInterface + */ + private $loggerMock; + protected function setUp() { $this->_observerFactoryMock = $this->createMock(\Magento\Framework\Event\ObserverFactory::class); @@ -41,10 +49,12 @@ protected function setUp() ['execute'] ); $this->_appStateMock = $this->createMock(\Magento\Framework\App\State::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); $this->_invokerDefault = new \Magento\Framework\Event\Invoker\InvokerDefault( $this->_observerFactoryMock, - $this->_appStateMock + $this->_appStateMock, + $this->loggerMock ); } @@ -166,13 +176,15 @@ public function testWrongInterfaceCallWithDisabledDeveloperMode($shared) $this->returnValue($notObserver) ); $this->_appStateMock->expects( - $this->once() + $this->exactly(1) )->method( 'getMode' )->will( $this->returnValue(\Magento\Framework\App\State::MODE_PRODUCTION) ); + $this->loggerMock->expects($this->once())->method('warning'); + $this->_invokerDefault->dispatch( [ 'shared' => $shared, diff --git a/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php b/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php index d945791282a2d..023c4cc4ddba6 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php @@ -3,24 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\File\Test\Unit\Transfer\Adapter; -use \Magento\Framework\File\Transfer\Adapter\Http; +use Magento\Framework\File\Transfer\Adapter\Http; +use Magento\Framework\File\Mime; +use Magento\Framework\HTTP\PhpEnvironment\Response; +use Magento\Framework\App\Request\Http as RequestHttp; +use PHPUnit\Framework\MockObject\MockObject; +/** + * Tests http transfer adapter. + */ class HttpTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\HTTP\PhpEnvironment\Response|\PHPUnit_Framework_MockObject_MockObject + * @var RequestHttp|MockObject + */ + private $request; + + /** + * @var Response|MockObject */ private $response; /** - * @var Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|MockObject */ private $object; /** - * @var \Magento\Framework\File\Mime|\PHPUnit_Framework_MockObject_MockObject + * @var Mime|MockObject */ private $mime; @@ -30,11 +43,15 @@ class HttpTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->response = $this->createPartialMock( - \Magento\Framework\HTTP\PhpEnvironment\Response::class, + Response::class, ['setHeader', 'sendHeaders', 'setHeaders'] ); - $this->mime = $this->createMock(\Magento\Framework\File\Mime::class); - $this->object = new Http($this->response, $this->mime); + $this->mime = $this->createMock(Mime::class); + $this->request = $this->createPartialMock( + RequestHttp::class, + ['isHead'] + ); + $this->object = new Http($this->response, $this->mime, $this->request); } /** @@ -56,7 +73,10 @@ public function testSend(): void $this->mime->expects($this->once()) ->method('getMimeType') ->with($file) - ->will($this->returnValue($contentType)); + ->willReturn($contentType); + $this->request->expects($this->once()) + ->method('isHead') + ->willReturn(false); $this->expectOutputString(file_get_contents($file)); $this->object->send($file); @@ -82,7 +102,10 @@ public function testSendWithOptions(): void $this->mime->expects($this->once()) ->method('getMimeType') ->with($file) - ->will($this->returnValue($contentType)); + ->willReturn($contentType); + $this->request->expects($this->once()) + ->method('isHead') + ->willReturn(false); $this->expectOutputString(file_get_contents($file)); $this->object->send(['filepath' => $file, 'headers' => $headers]); @@ -106,4 +129,32 @@ public function testSendNoFileExistException(): void { $this->object->send('nonexistent.file'); } + + /** + * @return void + */ + public function testSendHeadRequest(): void + { + $file = __DIR__ . '/../../_files/javascript.js'; + $contentType = 'content/type'; + + $this->response->expects($this->at(0)) + ->method('setHeader') + ->with('Content-length', filesize($file)); + $this->response->expects($this->at(1)) + ->method('setHeader') + ->with('Content-Type', $contentType); + $this->response->expects($this->once()) + ->method('sendHeaders'); + $this->mime->expects($this->once()) + ->method('getMimeType') + ->with($file) + ->willReturn($contentType); + $this->request->expects($this->once()) + ->method('isHead') + ->willReturn(true); + + $this->object->send($file); + $this->assertEquals(false, $this->hasOutput()); + } } diff --git a/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php b/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php index aa527866eff55..cd42c8d04b477 100644 --- a/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php +++ b/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php @@ -6,28 +6,46 @@ namespace Magento\Framework\File\Transfer\Adapter; +use Magento\Framework\HTTP\PhpEnvironment\Response; +use Magento\Framework\File\Mime; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\ObjectManager; +use Zend\Http\Headers; + +/** + * File adapter to send the file to the client. + */ class Http { /** - * @var \Magento\Framework\HTTP\PhpEnvironment\Response + * @var Response */ private $response; /** - * @var \Magento\Framework\File\Mime + * @var Mime */ private $mime; /** - * @param \Magento\Framework\App\Response\Http $response - * @param \Magento\Framework\File\Mime $mime + * @var HttpRequest + */ + private $request; + + /** + * @param Response $response + * @param Mime $mime + * @param HttpRequest|null $request */ public function __construct( - \Magento\Framework\HTTP\PhpEnvironment\Response $response, - \Magento\Framework\File\Mime $mime + Response $response, + Mime $mime, + HttpRequest $request = null ) { $this->response = $response; $this->mime = $mime; + $objectManager = ObjectManager::getInstance(); + $this->request = $request ?: $objectManager->get(HttpRequest::class); } /** @@ -46,18 +64,17 @@ public function send($options = null) throw new \InvalidArgumentException("File '{$filepath}' does not exists."); } - $mimeType = $this->mime->getMimeType($filepath); - if (is_array($options) && isset($options['headers']) && $options['headers'] instanceof \Zend\Http\Headers) { - $this->response->setHeaders($options['headers']); - } - $this->response->setHeader('Content-length', filesize($filepath)); - $this->response->setHeader('Content-Type', $mimeType); + $this->prepareResponse($options, $filepath); - $this->response->sendHeaders(); + if ($this->request->isHead()) { + // Do not send the body on HEAD requests. + return; + } $handle = fopen($filepath, 'r'); if ($handle) { while (($buffer = fgets($handle, 4096)) !== false) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $buffer; } if (!feof($handle)) { @@ -88,4 +105,22 @@ private function getFilePath($options): string return $filePath; } + + /** + * Set and send all necessary headers. + * + * @param array $options + * @param string $filepath + */ + private function prepareResponse($options, string $filepath): void + { + $mimeType = $this->mime->getMimeType($filepath); + if (is_array($options) && isset($options['headers']) && $options['headers'] instanceof Headers) { + $this->response->setHeaders($options['headers']); + } + $this->response->setHeader('Content-length', filesize($filepath)); + $this->response->setHeader('Content-Type', $mimeType); + + $this->response->sendHeaders(); + } } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php index 61108c64dda44..85d41b6932629 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php @@ -89,6 +89,7 @@ public function isDirectory($path = null); * * @param string $path * @return \Magento\Framework\Filesystem\File\ReadInterface + * @throws \Magento\Framework\Exception\FileSystemException */ public function openFile($path); diff --git a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php index 20874f60791c1..b3ab51b948109 100644 --- a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php @@ -8,6 +8,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Filesystem; /** @@ -96,7 +97,8 @@ public function __construct($root, array $config = []) static::validate($config); $this->root = $this->normalizePath($root); $this->directories = static::getDefaultConfig(); - $this->directories[self::SYS_TMP] = [self::PATH => realpath(sys_get_temp_dir())]; + $sysTmpPath = get_cfg_var('upload_tmp_dir') ?: sys_get_temp_dir(); + $this->directories[self::SYS_TMP] = [self::PATH => realpath($sysTmpPath)]; // inject custom values from constructor foreach ($this->directories as $code => $dir) { diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/Http.php b/lib/internal/Magento/Framework/Filesystem/Driver/Http.php index 3668bd17477a4..f32624f4e7513 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/Http.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/Http.php @@ -27,26 +27,18 @@ class Http extends File * * @param string $path * @return bool - * @throws FileSystemException */ public function isExists($path) { $headers = array_change_key_case(get_headers($this->getScheme() . $path, 1), CASE_LOWER); - $status = $headers[0]; - /* Handling 302 redirection */ - if (strpos($status, '302 Found') !== false && isset($headers[1])) { + /* Handling 301 or 302 redirection */ + if (isset($headers[1]) && preg_match('/30[12]/', $status)) { $status = $headers[1]; } - if (strpos($status, '200 OK') === false) { - $result = false; - } else { - $result = true; - } - - return $result; + return !(strpos($status, '200 OK') === false); } /** diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index 3e5f9bcf0bd27..d3a8d5334ab9d 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -9,6 +9,9 @@ */ namespace Magento\Framework\Filter; +use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\Model\AbstractModel; + /** * Template filter * @@ -63,6 +66,18 @@ class Template implements \Zend_Filter_Interface */ protected $string; + /** + * @var string[] + */ + private $restrictedMethods = [ + 'addafterfiltercallback', + 'getresourcecollection', + 'load', + 'save', + 'getcollection', + 'getresource' + ]; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param array $variables @@ -293,7 +308,7 @@ public function templateDirective($construction) { // Processing of {template config_path=... [...]} statement $templateParameters = $this->getParameters($construction[2]); - if (!isset($templateParameters['config_path']) or !$this->getTemplateProcessor()) { + if (!isset($templateParameters['config_path']) || !$this->getTemplateProcessor()) { // Not specified template or not set include processor $replacedValue = '{Error in template processing}'; } else { @@ -367,6 +382,46 @@ protected function getParameters($value) return $params; } + /** + * Validate method call initiated in a template. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return void + * @throws \InvalidArgumentException + */ + private function validateVariableMethodCall($object, string $method): void + { + if ($object === $this) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + } + + /** + * Check allowed methods for data objects. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return bool + * @throws \InvalidArgumentException + */ + private function isAllowedDataObjectMethod($object, string $method): bool + { + if ($object instanceof AbstractExtensibleModel || $object instanceof AbstractModel) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + + return true; + } + /** * Return variable value for var construction * @@ -405,21 +460,27 @@ protected function getVariable($value, $default = '{no_value_defined}') || substr($stackVars[$i]['name'], 0, 3) == 'get' ) { $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + + if ($this->isAllowedDataObjectMethod($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { + $stackVars[$i]['variable'] = call_user_func_array( + [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], + $stackVars[$i]['args'] + ); + } } } $last = $i; - } elseif (isset($stackVars[$i - 1]['variable']) && $stackVars[$i]['type'] == 'method') { + } elseif (isset($stackVars[$i - 1]['variable']) + && is_object($stackVars[$i - 1]['variable']) + && $stackVars[$i]['type'] == 'method' + ) { // Calling object methods - if (method_exists($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { - $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + $object = $stackVars[$i - 1]['variable']; + $method = $stackVars[$i]['name']; + if (method_exists($object, $method)) { + $args = $this->getStackArgs($stackVars[$i]['args']); + $this->validateVariableMethodCall($object, $method); + $stackVars[$i]['variable'] = call_user_func_array([$object, $method], $args); } $last = $i; } diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/StripTagsTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/StripTagsTest.php index f0dc0d9bd7874..2c42eb3d1c8db 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/StripTagsTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/StripTagsTest.php @@ -12,8 +12,7 @@ class StripTagsTest extends \PHPUnit\Framework\TestCase */ public function testStripTags() { - $escaper = $this->createMock(\Magento\Framework\Escaper::class); - $stripTags = new \Magento\Framework\Filter\StripTags($escaper); + $stripTags = new \Magento\Framework\Filter\StripTags(new \Magento\Framework\Escaper()); $this->assertEquals('three', $stripTags->filter('three')); } } diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php index 4883dc5fbe33b..e4a2dc48d11dd 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php @@ -6,6 +6,8 @@ namespace Magento\Framework\Filter\Test\Unit; +use Magento\Store\Model\Store; + class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -13,10 +15,16 @@ class TemplateTest extends \PHPUnit\Framework\TestCase */ private $templateFilter; + /** + * @var Store + */ + private $store; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->templateFilter = $objectManager->getObject(\Magento\Framework\Filter\Template::class); + $this->store = $objectManager->getObject(Store::class); } public function testFilter() @@ -380,4 +388,55 @@ private function getObjectData() $dataObject->setAllVisibleItems($visibleItems); return $dataObject; } + + /** + * Check that if calling a method of an object fails expected result is returned. + */ + public function testInvalidMethodCall() + { + $this->templateFilter->setVariables(['dateTime' => '\DateTime']); + $this->assertEquals( + '\DateTime', + $this->templateFilter->filter('{{var dateTime.createFromFormat(\'d\',\'1548201468\')}}') + ); + } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + */ + public function testInappropriateCallbacks() + { + $this->templateFilter->setVariables(['filter' => $this->templateFilter]); + $this->templateFilter->filter('Test {{var filter.addAfterFilterCallback(\'mb_strtolower\')}}'); + } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + * @dataProvider disallowedMethods + */ + public function testDisallowedMethods($method) + { + $this->templateFilter->setVariables(['store' => $this->store]); + $this->templateFilter->filter('{{var store.'.$method.'()}}'); + } + + /** + * Data for testDisallowedMethods method + * + * @return array + */ + public function disallowedMethods() + { + return [ + ['getResourceCollection'], + ['load'], + ['save'], + ['getCollection'], + ['getResource'], + ]; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config.php b/lib/internal/Magento/Framework/GraphQl/Config.php index 75b6c64e9d24f..ec22b742b1d6c 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config.php +++ b/lib/internal/Magento/Framework/GraphQl/Config.php @@ -48,12 +48,7 @@ public function __construct( } /** - * Get a data object with data pertaining to a GraphQL type's structural makeup. - * - * @param string $configElementName - * @return ConfigElementInterface - * @throws \LogicException - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @inheritdoc */ public function getConfigElement(string $configElementName) : ConfigElementInterface { @@ -67,7 +62,7 @@ public function getConfigElement(string $configElementName) : ConfigElementInter $fieldsInQuery = $this->queryFields->getFieldsUsedInQuery(); if (isset($data['fields'])) { if (!empty($fieldsInQuery)) { - foreach ($data['fields'] as $fieldName => $fieldConfig) { + foreach (array_keys($data['fields']) as $fieldName) { if (!isset($fieldsInQuery[$fieldName])) { unset($data['fields'][$fieldName]); } @@ -81,18 +76,20 @@ public function getConfigElement(string $configElementName) : ConfigElementInter } /** - * Return all type names declared in a GraphQL schema's configuration. - * - * @return string[] + * @inheritdoc */ - public function getDeclaredTypeNames() : array + public function getDeclaredTypes() : array { $types = []; foreach ($this->configData->get(null) as $item) { - if (isset($item['type']) && $item['type'] == 'graphql_type') { - $types[] = $item['name']; + if (isset($item['type'])) { + $types[] = [ + 'name' => $item['name'], + 'type' => $item['type'], + ]; } } + return $types; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php index b1210e986b772..994ae489af128 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php @@ -37,7 +37,7 @@ class Enum implements ConfigElementInterface public function __construct( string $name, array $values, - string $description = "" + string $description ) { $this->name = $name; $this->values = $values; diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldsFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldsFactory.php new file mode 100644 index 0000000000000..ca6b67eac3d83 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldsFactory.php @@ -0,0 +1,62 @@ +argumentFactory = $argumentFactory; + $this->fieldFactory = $fieldFactory; + } + + /** + * Create a fields object from a configured array with optional arguments. + * + * Field data must contain name and type. Other values are optional and include required, itemType, description, + * and resolver. Arguments array must be in the format of [$argumentData['name'] => $argumentData]. + * + * @param array $fieldsData + * @return Field[] + */ + public function createFromConfigData( + array $fieldsData + ) : array { + $fields = []; + foreach ($fieldsData as $fieldData) { + $arguments = []; + foreach ($fieldData['arguments'] as $argumentData) { + $arguments[$argumentData['name']] = $this->argumentFactory->createFromConfigData($argumentData); + } + $fields[$fieldData['name']] = $this->fieldFactory->createFromConfigData( + $fieldData, + $arguments + ); + } + return $fields; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php new file mode 100644 index 0000000000000..8e86f701672c6 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php @@ -0,0 +1,74 @@ +name = $name; + $this->fields = $fields; + $this->description = $description; + } + + /** + * Get the type name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get a list of fields that make up the possible return or input values of a type. + * + * @return Field[] + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * Get a human-readable description of the type. + * + * @return string + */ + public function getDescription(): string + { + return $this->description; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php new file mode 100644 index 0000000000000..0e7ccb831a5a4 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php @@ -0,0 +1,79 @@ +objectManager = $objectManager; + $this->fieldsFactory = $fieldsFactory; + } + + /** + * Instantiate an object representing 'input' GraphQL config element. + * + * @param array $data + * @return ConfigElementInterface + */ + public function createFromConfigData(array $data): ConfigElementInterface + { + $fields = isset($data['fields']) ? $this->fieldsFactory->createFromConfigData($data['fields']) : []; + + return $this->create( + $data, + $fields + ); + } + + /** + * Create input type object based off array of configured GraphQL InputType data. + * + * Type data must contain name and the type's fields. Optional data includes description. + * + * @param array $typeData + * @param array $fields + * @return Input + */ + private function create( + array $typeData, + array $fields + ): Input { + return $this->objectManager->create( + Input::class, + [ + 'name' => $typeData['name'], + 'fields' => $fields, + 'description' => isset($typeData['description']) ? $typeData['description'] : '' + ] + ); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php index 320199c14a6d6..73ebd42acfb27 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php @@ -8,7 +8,7 @@ namespace Magento\Framework\GraphQl\Config\Element; /** - * Describes the configured data for a GraphQL interface type. + * Class representing 'interface' GraphQL config element. */ class InterfaceType implements TypeInterface { @@ -42,7 +42,7 @@ public function __construct( string $name, string $typeResolver, array $fields, - string $description = "" + string $description ) { $this->name = $name; $this->fields = $fields; diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php index 24ff439db0347..20d017cc71062 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php @@ -8,7 +8,7 @@ namespace Magento\Framework\GraphQl\Config\Element; /** - * Describes all the configured data of an Output or Input type in GraphQL. + * Class representing 'type' GraphQL config element. */ class Type implements TypeInterface { diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php index c5f3187b04841..5dd477a050890 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php @@ -22,28 +22,20 @@ class TypeFactory implements ConfigElementFactoryInterface private $objectManager; /** - * @var ArgumentFactory + * @var FieldsFactory */ - private $argumentFactory; - - /** - * @var FieldFactory - */ - private $fieldFactory; + private $fieldsFactory; /** * @param ObjectManagerInterface $objectManager - * @param ArgumentFactory $argumentFactory - * @param FieldFactory $fieldFactory + * @param FieldsFactory $fieldsFactory */ public function __construct( ObjectManagerInterface $objectManager, - ArgumentFactory $argumentFactory, - FieldFactory $fieldFactory + FieldsFactory $fieldsFactory ) { $this->objectManager = $objectManager; - $this->argumentFactory = $argumentFactory; - $this->fieldFactory = $fieldFactory; + $this->fieldsFactory = $fieldsFactory; } /** @@ -54,18 +46,8 @@ public function __construct( */ public function createFromConfigData(array $data): ConfigElementInterface { - $fields = []; - $data['fields'] = isset($data['fields']) ? $data['fields'] : []; - foreach ($data['fields'] as $field) { - $arguments = []; - foreach ($field['arguments'] as $argument) { - $arguments[$argument['name']] = $this->argumentFactory->createFromConfigData($argument); - } - $fields[$field['name']] = $this->fieldFactory->createFromConfigData( - $field, - $arguments - ); - } + $fields = isset($data['fields']) ? $this->fieldsFactory->createFromConfigData($data['fields']) : []; + return $this->create( $data, $fields @@ -73,10 +55,10 @@ public function createFromConfigData(array $data): ConfigElementInterface } /** - * Create type object based off array of configured GraphQL Output/InputType data. + * Create type object based off array of configured GraphQL Type data. * * Type data must contain name and the type's fields. Optional data includes 'implements' (i.e. the interfaces - * implemented by the types), and description. An InputType cannot implement an interface. + * implemented by the types), and description. * * @param array $typeData * @param array $fields diff --git a/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php b/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php index c2670967f1db5..f7d6cf49e180c 100644 --- a/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php +++ b/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php @@ -25,9 +25,11 @@ interface ConfigInterface public function getConfigElement(string $configElementName) : ConfigElementInterface; /** - * Return all type names from a GraphQL schema's configuration. + * Return all type names declared in a GraphQL schema's configuration and their type. * - * @return string[] + * Format is ['name' => 'example value', 'type' = 'example value'] + * + * @return array $types */ - public function getDeclaredTypeNames() : array; + public function getDeclaredTypes() : array; } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Fields.php b/lib/internal/Magento/Framework/GraphQl/Query/Fields.php index d0bc9591265eb..a34c0a9d42187 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Fields.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Fields.php @@ -24,9 +24,11 @@ class Fields * Set Query for extracting list of fields. * * @param string $query + * @param array|null $variables + * * @return void */ - public function setQuery($query) + public function setQuery($query, array $variables = null) { $queryFields = []; try { @@ -41,6 +43,9 @@ public function setQuery($query) ] ] ); + if (isset($variables)) { + $queryFields = array_merge($queryFields, $this->extractVariables($variables)); + } } catch (\Exception $e) { // If a syntax error is encountered do not collect fields } @@ -62,4 +67,24 @@ public function getFieldsUsedInQuery() { return $this->fieldsUsedInQuery; } + + /** + * Extract and return list of all used fields in GraphQL query's variables + * + * @param array $variables + * + * @return string[] + */ + private function extractVariables(array $variables): array + { + $fields = []; + foreach ($variables as $key => $value) { + if (is_array($value)) { + $fields = array_merge($fields, $this->extractVariables($value)); + } + $fields[$key] = $key; + } + + return $fields; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php b/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php new file mode 100644 index 0000000000000..2fdb3df5f6d71 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php @@ -0,0 +1,42 @@ +deploymentConfig = $deploymentConfig; + } + + /** + * Check the the environment config to determine if introspection should be disabled. + * + * @return bool + */ + public function isIntrospectionDisabled(): bool + { + return (bool)$this->deploymentConfig->get(self::CONFIG_PATH_DISABLE_INTROSPECTION); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php index 5730156ca5b34..2b9ce9b01b5c4 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php @@ -33,16 +33,24 @@ class QueryComplexityLimiter */ private $queryComplexity; + /** + * @var IntrospectionConfiguration + */ + private $introspectionConfig; + /** * @param int $queryDepth * @param int $queryComplexity + * @param IntrospectionConfiguration $introspectionConfig */ public function __construct( int $queryDepth, - int $queryComplexity + int $queryComplexity, + IntrospectionConfiguration $introspectionConfig ) { $this->queryDepth = $queryDepth; $this->queryComplexity = $queryComplexity; + $this->introspectionConfig = $introspectionConfig; } /** @@ -53,7 +61,9 @@ public function __construct( public function execute(): void { DocumentValidator::addRule(new QueryComplexity($this->queryComplexity)); - DocumentValidator::addRule(new DisableIntrospection()); + DocumentValidator::addRule( + new DisableIntrospection((int) $this->introspectionConfig->isIntrospectionDisabled()) + ); DocumentValidator::addRule(new QueryDepth($this->queryDepth)); } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php index a6ad10dded849..0a0dba36ef0ed 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php @@ -69,7 +69,7 @@ public function process( $operationName )->toArray( $this->exceptionFormatter->shouldShowDetail() ? - \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\Debug::INCLUDE_TRACE : false + \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE : false ); } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php index e7d14a81b9dee..bd9de206ccda1 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php @@ -38,7 +38,7 @@ public function getEntityAttributesForEntityFromField(string $fieldName) : array if (isset($this->attributesInstances[$fieldName])) { return $this->attributesInstances[$fieldName]->getEntityAttributes(); } else { - throw new \LogicException(sprintf('There is no attrribute class assigned to field %1', $fieldName)); + throw new \LogicException(sprintf('There is no attribute class assigned to field %1', $fieldName)); } } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php b/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php index 63fef73186b12..250b80defa6dd 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php @@ -8,9 +8,8 @@ namespace Magento\Framework\GraphQl\Schema; use Magento\Framework\GraphQl\ConfigInterface; -use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface; -use Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper; use Magento\Framework\GraphQl\Schema; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; use Magento\Framework\GraphQl\SchemaFactory; /** @@ -24,47 +23,46 @@ class SchemaGenerator implements SchemaGeneratorInterface private $schemaFactory; /** - * @var OutputMapper + * @var ConfigInterface */ - private $outputMapper; + private $config; /** - * @var ConfigInterface + * @var TypeRegistry */ - private $config; + private $typeRegistry; /** * @param SchemaFactory $schemaFactory - * @param OutputMapper $outputMapper * @param ConfigInterface $config + * @param TypeRegistry $typeRegistry */ public function __construct( SchemaFactory $schemaFactory, - OutputMapper $outputMapper, - ConfigInterface $config + ConfigInterface $config, + TypeRegistry $typeRegistry ) { $this->schemaFactory = $schemaFactory; - $this->outputMapper = $outputMapper; $this->config = $config; + $this->typeRegistry = $typeRegistry; } /** - * {@inheritdoc} + * @inheritdoc */ public function generate() : Schema { $schema = $this->schemaFactory->create( [ - 'query' => $this->outputMapper->getOutputType('Query'), - 'mutation' => $this->outputMapper->getOutputType('Mutation'), + 'query' => $this->typeRegistry->get('Query'), + 'mutation' => $this->typeRegistry->get('Mutation'), 'typeLoader' => function ($name) { - return $this->outputMapper->getOutputType($name); + return $this->typeRegistry->get($name); }, 'types' => function () { - //all types should be generated only on introspection $typesImplementors = []; - foreach ($this->config->getDeclaredTypeNames() as $name) { - $typesImplementors [] = $this->outputMapper->getOutputType($name); + foreach ($this->config->getDeclaredTypes() as $type) { + $typesImplementors [] = $this->typeRegistry->get($type['name']); } return $typesImplementors; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputFactory.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputFactory.php deleted file mode 100644 index cbbd97cfdb8c7..0000000000000 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputFactory.php +++ /dev/null @@ -1,60 +0,0 @@ -objectManager = $objectManager; - $this->prototypes = $prototypes; - } - - /** - * @param ConfigElementInterface $configElement - * @return InputTypeInterface - */ - public function create(ConfigElementInterface $configElement) : InputTypeInterface - { - if (!isset($this->typeRegistry[$configElement->getName()])) { - $this->typeRegistry[$configElement->getName()] = - $this->objectManager->create( - $this->prototypes[get_class($configElement)], - [ - 'configElement' => $configElement - ] - ); - } - return $this->typeRegistry[$configElement->getName()]; - } -} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php index d806c0b3e68ab..d1f48dada2cbd 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php @@ -9,27 +9,15 @@ use Magento\Framework\GraphQl\Config\Data\WrappedTypeProcessor; use Magento\Framework\GraphQl\Config\Element\Argument; -use Magento\Framework\GraphQl\ConfigInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ScalarTypes; -use Magento\Framework\GraphQl\Schema\TypeFactory; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; +/** + * Prepare argument's metadata for GraphQL schema generation + */ class InputMapper { - /** - * @var InputFactory - */ - private $inputFactory; - - /** - * @var ConfigInterface - */ - private $config; - - /** - * @var TypeFactory - */ - private $typeFactory; - /** * @var ScalarTypes */ @@ -41,24 +29,23 @@ class InputMapper private $wrappedTypeProcessor; /** - * @param InputFactory $inputFactory - * @param ConfigInterface $config - * @param TypeFactory $typeFactory + * @var TypeRegistry + */ + private $typeRegistry; + + /** * @param ScalarTypes $scalarTypes * @param WrappedTypeProcessor $wrappedTypeProcessor + * @param TypeRegistry $typeRegistry */ public function __construct( - InputFactory $inputFactory, - ConfigInterface $config, - TypeFactory $typeFactory, ScalarTypes $scalarTypes, - WrappedTypeProcessor $wrappedTypeProcessor + WrappedTypeProcessor $wrappedTypeProcessor, + TypeRegistry $typeRegistry ) { - $this->inputFactory = $inputFactory; - $this->config = $config; - $this->typeFactory = $typeFactory; $this->scalarTypes = $scalarTypes; $this->wrappedTypeProcessor = $wrappedTypeProcessor; + $this->typeRegistry = $typeRegistry; } /** @@ -66,6 +53,7 @@ public function __construct( * * @param Argument $argument * @return array + * @throws GraphQlInputException */ public function getRepresentation(Argument $argument) : array { @@ -73,8 +61,7 @@ public function getRepresentation(Argument $argument) : array if ($this->scalarTypes->isScalarType($typeName)) { $instance = $this->wrappedTypeProcessor->processScalarWrappedType($argument); } else { - $configElement = $this->config->getConfigElement($typeName); - $instance = $this->inputFactory->create($configElement); + $instance = $this->typeRegistry->get($typeName); $instance = $this->wrappedTypeProcessor->processWrappedType($argument, $instance); } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php index ae2d07ade2ad0..fa0327f79bc66 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php @@ -8,21 +8,16 @@ namespace Magento\Framework\GraphQl\Schema\Type\Input; use Magento\Framework\GraphQl\Config\Data\WrappedTypeProcessor; -use Magento\Framework\GraphQl\Config\Element\Type as TypeConfigElement; -use Magento\Framework\GraphQl\ConfigInterface; +use Magento\Framework\GraphQl\Config\Element\Input as InputConfigElement; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ScalarTypes; -use Magento\Framework\GraphQl\Schema\TypeFactory; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; /** * Class InputObjectType */ class InputObjectType extends \Magento\Framework\GraphQl\Schema\Type\InputObjectType { - /** - * @var TypeFactory - */ - private $typeFactory; - /** * @var ScalarTypes */ @@ -34,36 +29,27 @@ class InputObjectType extends \Magento\Framework\GraphQl\Schema\Type\InputObject private $wrappedTypeProcessor; /** - * @var InputFactory + * @var TypeRegistry */ - private $inputFactory; + private $typeRegistry; /** - * @var ConfigInterface - */ - public $graphQlConfig; - - /** - * @param TypeConfigElement $configElement - * @param TypeFactory $typeFactory + * @param InputConfigElement $configElement * @param ScalarTypes $scalarTypes * @param WrappedTypeProcessor $wrappedTypeProcessor - * @param InputFactory $inputFactory - * @param ConfigInterface $graphQlConfig + * @param TypeRegistry $typeRegistry + * @throws GraphQlInputException */ public function __construct( - TypeConfigElement $configElement, - TypeFactory $typeFactory, + InputConfigElement $configElement, ScalarTypes $scalarTypes, WrappedTypeProcessor $wrappedTypeProcessor, - InputFactory $inputFactory, - ConfigInterface $graphQlConfig + TypeRegistry $typeRegistry ) { - $this->typeFactory = $typeFactory; $this->scalarTypes = $scalarTypes; $this->wrappedTypeProcessor = $wrappedTypeProcessor; - $this->inputFactory = $inputFactory; - $this->graphQlConfig = $graphQlConfig; + $this->typeRegistry = $typeRegistry; + $config = [ 'name' => $configElement->getName(), 'description' => $configElement->getDescription() @@ -75,8 +61,7 @@ public function __construct( if ($field->getTypeName() == $configElement->getName()) { $type = $this; } else { - $fieldConfigElement = $this->graphQlConfig->getConfigElement($field->getTypeName()); - $type = $this->inputFactory->create($fieldConfigElement); + $type = $this->typeRegistry->get($field->getTypeName()); } $type = $this->wrappedTypeProcessor->processWrappedType($field, $type); } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php index b54cd4d8ca218..034a5702090d9 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php @@ -16,7 +16,6 @@ use Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; use Magento\Framework\GraphQl\Schema\Type\ScalarTypes; -use Magento\Framework\GraphQl\Schema\TypeFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfoFactory; @@ -40,11 +39,6 @@ class Fields implements FormatterInterface */ private $inputMapper; - /** - * @var TypeFactory - */ - private $typeFactory; - /** * @var ScalarTypes */ @@ -64,7 +58,6 @@ class Fields implements FormatterInterface * @param ObjectManagerInterface $objectManager * @param OutputMapper $outputMapper * @param InputMapper $inputMapper - * @param TypeFactory $typeFactory * @param ScalarTypes $scalarTypes * @param WrappedTypeProcessor $wrappedTypeProcessor * @param ResolveInfoFactory $resolveInfoFactory @@ -73,7 +66,6 @@ public function __construct( ObjectManagerInterface $objectManager, OutputMapper $outputMapper, InputMapper $inputMapper, - TypeFactory $typeFactory, ScalarTypes $scalarTypes, WrappedTypeProcessor $wrappedTypeProcessor, ResolveInfoFactory $resolveInfoFactory @@ -81,14 +73,13 @@ public function __construct( $this->objectManager = $objectManager; $this->outputMapper = $outputMapper; $this->inputMapper = $inputMapper; - $this->typeFactory = $typeFactory; $this->scalarTypes = $scalarTypes; $this->wrappedTypeProcessor = $wrappedTypeProcessor; $this->resolveInfoFactory = $resolveInfoFactory; } /** - * {@inheritDoc} + * @inheritdoc */ public function format(TypeInterface $configElement, OutputTypeInterface $outputType): array { diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputFactory.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputFactory.php deleted file mode 100644 index 81dad11774b01..0000000000000 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputFactory.php +++ /dev/null @@ -1,65 +0,0 @@ -objectManager = $objectManager; - $this->prototypes = $prototypes; - } - - /** - * Create output type. - * - * @param ConfigElementInterface $configElement - * @return OutputTypeInterface - */ - public function create(ConfigElementInterface $configElement) : OutputTypeInterface - { - if (!isset($this->typeRegistry[$configElement->getName()])) { - $this->typeRegistry[$configElement->getName()] = - $this->objectManager->create( - $this->prototypes[get_class($configElement)], - [ - 'configElement' => $configElement - ] - ); - } - return $this->typeRegistry[$configElement->getName()]; - } -} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php index b7f4b8a1f60db..046eeb5b1f93d 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php @@ -7,50 +7,28 @@ namespace Magento\Framework\GraphQl\Schema\Type\Output; -use Magento\Framework\GraphQl\ConfigInterface; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; -use Magento\Framework\GraphQl\Schema\TypeFactory; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\Phrase; /** - * Map type names to their output type/interface classes. + * Map type names to their output type/interface/enum classes. */ class OutputMapper { /** - * @var OutputFactory + * @var TypeRegistry */ - private $outputFactory; + private $typeRegistry; /** - * @var OutputTypeInterface[] - */ - private $outputTypes; - - /** - * @var TypeFactory - */ - private $typeFactory; - - /** - * @var ConfigInterface - */ - private $config; - - /** - * @param OutputFactory $outputFactory - * @param TypeFactory $typeFactory - * @param ConfigInterface $config + * @param TypeRegistry $typeRegistry */ public function __construct( - OutputFactory $outputFactory, - TypeFactory $typeFactory, - ConfigInterface $config + TypeRegistry $typeRegistry ) { - $this->outputFactory = $outputFactory; - $this->config = $config; - $this->typeFactory = $typeFactory; + $this->typeRegistry = $typeRegistry; } /** @@ -62,16 +40,13 @@ public function __construct( */ public function getOutputType($typeName) { - if (!isset($this->outputTypes[$typeName])) { - $configElement = $this->config->getConfigElement($typeName); - $this->outputTypes[$typeName] = $this->outputFactory->create($configElement); - if (!($this->outputTypes[$typeName] instanceof OutputTypeInterface)) { - throw new GraphQlInputException( - new Phrase("Type '{$typeName}' was requested but is not declared in the GraphQL schema.") - ); - } - } + $outputType = $this->typeRegistry->get($typeName); - return $this->outputTypes[$typeName]; + if (!$outputType instanceof OutputTypeInterface) { + throw new GraphQlInputException( + new Phrase("Type '{$typeName}' was requested but is not declared in the GraphQL schema.") + ); + } + return $outputType; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php new file mode 100644 index 0000000000000..cde8b6b3e446b --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php @@ -0,0 +1,95 @@ +objectManager = $objectManager; + $this->config = $config; + $this->configToTypeMap = $configToTypeMap; + } + + /** + * Get GraphQL type object by type name + * + * @param string $typeName + * @return TypeInterface|InputTypeInterface|OutputTypeInterface + * @throws GraphQlInputException + */ + public function get(string $typeName): TypeInterface + { + if (!isset($this->types[$typeName])) { + $configElement = $this->config->getConfigElement($typeName); + + $configElementClass = get_class($configElement); + if (!isset($this->configToTypeMap[$configElementClass])) { + throw new GraphQlInputException( + new Phrase( + "No mapping to Webonyx type is declared for '%1' config element type.", + [$configElementClass] + ) + ); + } + + $this->types[$typeName] = $this->objectManager->create( + $this->configToTypeMap[$configElementClass], + [ + 'configElement' => $configElement, + ] + ); + + if (!($this->types[$typeName] instanceof TypeInterface)) { + throw new GraphQlInputException( + new Phrase("Type '{$typeName}' was requested but is not declared in the GraphQL schema.") + ); + } + } + return $this->types[$typeName]; + } +} diff --git a/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php b/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php index 15f09b8505202..bc833bf3bb2d4 100644 --- a/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php +++ b/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php @@ -11,6 +11,9 @@ */ namespace Magento\Framework\HTTP\Adapter; +/** + * Curl http adapter + */ class Curl implements \Zend_Http_Client_Adapter_Interface { /** @@ -139,8 +142,8 @@ public function setConfig($config = []) /** * Connect to the remote server * - * @param string $host - * @param int $port + * @param string $host + * @param int $port * @param boolean $secure * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -273,7 +276,7 @@ public function getInfo($opt = 0) } /** - * curl_multi_* requests support + * Curl_multi_* requests support * * @param array $urls * @param array $options diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index 4dd358783a507..3ecf360f36894 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -14,7 +14,10 @@ use Zend\Uri\UriInterface; /** + * HTTP Request for current PHP environment. + * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Request extends \Zend\Http\PhpEnvironment\Request { @@ -586,6 +589,7 @@ public function setPostValue($name, $value = null) /** * Access values contained in the superglobals as public members + * * Order of precedence: 1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV * * @see http://msdn.microsoft.com/en-us/library/system.web.httprequest.item.aspx @@ -683,7 +687,7 @@ public function has($key) * * @param string $name Header name to retrieve. * @param mixed|null $default Default value to use when the requested header is missing. - * @return bool|HeaderInterface + * @return bool|string */ public function getHeader($name, $default = false) { @@ -795,6 +799,8 @@ public function getBaseUrl() } /** + * Get flag value for whether the request is forwarded or not. + * * @return bool * @codeCoverageIgnore */ @@ -804,6 +810,8 @@ public function isForwarded() } /** + * Set flag value for whether the request is forwarded or not. + * * @param bool $forwarded * @return $this * @codeCoverageIgnore diff --git a/lib/internal/Magento/Framework/Interception/Config/CacheManager.php b/lib/internal/Magento/Framework/Interception/Config/CacheManager.php index a754215bbe743..fd612370d0757 100644 --- a/lib/internal/Magento/Framework/Interception/Config/CacheManager.php +++ b/lib/internal/Magento/Framework/Interception/Config/CacheManager.php @@ -98,7 +98,7 @@ public function saveCompiled(string $key, array $data) */ public function clean(string $key) { - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [$key]); + $this->cache->remove($key); } /** diff --git a/lib/internal/Magento/Framework/Locale/Format.php b/lib/internal/Magento/Framework/Locale/Format.php index ca50cdb2440f4..adcffe01b910e 100644 --- a/lib/internal/Magento/Framework/Locale/Format.php +++ b/lib/internal/Magento/Framework/Locale/Format.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Locale; +/** + * Price locale format. + */ class Format implements \Magento\Framework\Locale\FormatInterface { /** @@ -38,7 +41,8 @@ public function __construct( } /** - * Returns the first found number from a string + * Returns the first found number from a string. + * * Parsing depends on given locale (grouping and decimal) * * Examples for input: @@ -100,7 +104,7 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) } $formatter = new \NumberFormatter( - $localeCode . '@currency=' . $currency->getCode(), + $currency->getCode() ? $localeCode . '@currency=' . $currency->getCode() : $localeCode, \NumberFormatter::CURRENCY ); $format = $formatter->getPattern(); diff --git a/lib/internal/Magento/Framework/Locale/Resolver.php b/lib/internal/Magento/Framework/Locale/Resolver.php index b401da8960f05..d058bfd41ab1a 100644 --- a/lib/internal/Magento/Framework/Locale/Resolver.php +++ b/lib/internal/Magento/Framework/Locale/Resolver.php @@ -6,7 +6,12 @@ namespace Magento\Framework\Locale; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +/** + * Manages locale config information. + */ class Resolver implements ResolverInterface { /** @@ -52,26 +57,34 @@ class Resolver implements ResolverInterface */ private $defaultLocalePath; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * @param ScopeConfigInterface $scopeConfig * @param string $defaultLocalePath * @param string $scopeType * @param mixed $locale + * @param DeploymentConfig|null $deploymentConfig */ public function __construct( ScopeConfigInterface $scopeConfig, $defaultLocalePath, $scopeType, - $locale = null + $locale = null, + DeploymentConfig $deploymentConfig = null ) { $this->scopeConfig = $scopeConfig; $this->defaultLocalePath = $defaultLocalePath; $this->scopeType = $scopeType; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->create(DeploymentConfig::class); $this->setLocale($locale); } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocalePath() { @@ -79,7 +92,7 @@ public function getDefaultLocalePath() } /** - * {@inheritdoc} + * @inheritdoc */ public function setDefaultLocale($locale) { @@ -88,12 +101,15 @@ public function setDefaultLocale($locale) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocale() { if (!$this->defaultLocale) { - $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + $locale = false; + if ($this->deploymentConfig->isAvailable() && $this->deploymentConfig->isDbAvailable()) { + $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + } if (!$locale) { $locale = self::DEFAULT_LOCALE; } @@ -103,7 +119,7 @@ public function getDefaultLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function setLocale($locale = null) { @@ -116,7 +132,7 @@ public function setLocale($locale = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getLocale() { @@ -127,7 +143,7 @@ public function getLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function emulate($scopeId) { @@ -147,7 +163,7 @@ public function emulate($scopeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function revert() { diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php index 1141f451c13a5..73a029a5a1411 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php @@ -68,15 +68,17 @@ protected function setUp() /** * @param string $localeCode + * @param string $currencyCode * @param array $expectedResult * @dataProvider getPriceFormatDataProvider */ - public function testGetPriceFormat($localeCode, array $expectedResult): void + public function testGetPriceFormat($localeCode, $currencyCode, array $expectedResult): void { $this->scope->expects($this->once()) ->method('getCurrentCurrency') ->willReturn($this->currency); + $this->currency->method('getCode')->willReturn($currencyCode); $result = $this->formatModel->getPriceFormat($localeCode); $intersection = array_intersect_assoc($result, $expectedResult); $this->assertCount(count($expectedResult), $intersection); @@ -88,18 +90,19 @@ public function testGetPriceFormat($localeCode, array $expectedResult): void */ public function getPriceFormatDataProvider(): array { + $swissGroupSymbol = INTL_ICU_VERSION >= 59.1 ? '’' : '\''; return [ - ['en_US', ['decimalSymbol' => '.', 'groupSymbol' => ',']], - ['de_DE', ['decimalSymbol' => ',', 'groupSymbol' => '.']], - ['de_CH', ['decimalSymbol' => '.', 'groupSymbol' => '\'']], - ['uk_UA', ['decimalSymbol' => ',', 'groupSymbol' => ' ']] + ['en_US', 'USD', ['decimalSymbol' => '.', 'groupSymbol' => ',']], + ['de_DE', 'EUR', ['decimalSymbol' => ',', 'groupSymbol' => '.']], + ['de_CH', 'CHF', ['decimalSymbol' => '.', 'groupSymbol' => $swissGroupSymbol]], + ['uk_UA', 'UAH', ['decimalSymbol' => ',', 'groupSymbol' => ' ']] ]; } /** * - * @param mixed $value - * @param float $expected + * @param mixed $value + * @param float $expected * @dataProvider provideNumbers */ public function testGetNumber($value, $expected): void diff --git a/lib/internal/Magento/Framework/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php new file mode 100644 index 0000000000000..dfe6bbb828352 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -0,0 +1,69 @@ +cache = $cache; + } + + /** + * @inheritdoc + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); + } + + /** + * @inheritdoc + */ + public function unlock(string $name): bool + { + return $this->cache->remove($this->getIdentifier($name)); + } + + /** + * @inheritdoc + */ + public function isLocked(string $name): bool + { + return (bool)$this->cache->test($this->getIdentifier($name)); + } + + /** + * Get cache locked identifier based on cache identifier. + * + * @param string $cacheIdentifier + * @return string + */ + private function getIdentifier(string $cacheIdentifier): string + { + return self::LOCK_PREFIX . $cacheIdentifier; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/Database.php b/lib/internal/Magento/Framework/Lock/Backend/Database.php index ce6aeca66d657..efdba63e7a081 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Database.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Database.php @@ -15,7 +15,7 @@ use Magento\Framework\Phrase; /** - * LockManager using the DB locks + * Implementation of the lock manager on the basis of MySQL. */ class Database implements \Magento\Framework\Lock\LockManagerInterface { @@ -62,9 +62,13 @@ public function __construct( * @return bool * @throws InputException * @throws AlreadyExistsException + * @throws \Zend_Db_Statement_Exception */ public function lock(string $name, int $timeout = -1): bool { + if (!$this->deploymentConfig->isDbAvailable()) { + return true; + }; $name = $this->addPrefix($name); /** @@ -75,7 +79,7 @@ public function lock(string $name, int $timeout = -1): bool if ($this->currentLock) { throw new AlreadyExistsException( new Phrase( - 'Current connection is already holding lock for $1, only single lock allowed', + 'Current connection is already holding lock for %1, only single lock allowed', [$this->currentLock] ) ); @@ -99,9 +103,14 @@ public function lock(string $name, int $timeout = -1): bool * @param string $name lock name * @return bool * @throws InputException + * @throws \Zend_Db_Statement_Exception */ public function unlock(string $name): bool { + if (!$this->deploymentConfig->isDbAvailable()) { + return true; + }; + $name = $this->addPrefix($name); $result = (bool)$this->resource->getConnection()->query( @@ -122,9 +131,14 @@ public function unlock(string $name): bool * @param string $name lock name * @return bool * @throws InputException + * @throws \Zend_Db_Statement_Exception */ public function isLocked(string $name): bool { + if (!$this->deploymentConfig->isDbAvailable()) { + return false; + }; + $name = $this->addPrefix($name); return (bool)$this->resource->getConnection()->query( diff --git a/lib/internal/Magento/Framework/Lock/Backend/FileLock.php b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php new file mode 100644 index 0000000000000..d168e910a4ab7 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php @@ -0,0 +1,194 @@ +fileDriver = $fileDriver; + $this->path = rtrim($path, '/') . '/'; + + try { + if (!$this->fileDriver->isExists($this->path)) { + $this->fileDriver->createDirectory($this->path); + } + } catch (FileSystemException $exception) { + throw new RuntimeException( + new Phrase('Cannot create the directory for locks: %1', [$this->path]), + $exception + ); + } + } + + /** + * Acquires a lock by name + * + * @param string $name The lock name + * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout + * @return bool Returns true if the lock is acquired, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems + */ + public function lock(string $name, int $timeout = -1): bool + { + try { + $lockFile = $this->getLockPath($name); + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + $skipDeadline = $timeout < 0; + $deadline = microtime(true) + $timeout; + + while (!$this->tryToLock($fileResource)) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->fileDriver->fileClose($fileResource); + return false; + } + usleep($this->sleepCycle); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); + } + + $this->locks[$lockFile] = $fileResource; + return true; + } + + /** + * Checks if a lock exists by name + * + * @param string $name The lock name + * @return bool Returns true if the lock exists, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists + */ + public function isLocked(string $name): bool + { + $lockFile = $this->getLockPath($name); + $result = false; + + try { + if ($this->fileDriver->isExists($lockFile)) { + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + if ($this->tryToLock($fileResource)) { + $result = false; + } else { + $result = true; + } + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); + } + + return $result; + } + + /** + * Remove the lock by name + * + * @param string $name The lock name + * @return bool If the lock is removed returns true, otherwise returns false + */ + public function unlock(string $name): bool + { + $lockFile = $this->getLockPath($name); + + if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { + unset($this->locks[$lockFile]); + return true; + } + + return false; + } + + /** + * Returns the full path to the lock file by name + * + * @param string $name The lock name + * @return string The path to the lock file + */ + private function getLockPath(string $name): string + { + return $this->path . $name; + } + + /** + * Tries to lock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is acquired returns true, otherwise returns false + */ + private function tryToLock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } + + /** + * Tries to unlock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is removed returns true, otherwise returns false + */ + private function tryToUnlock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php new file mode 100644 index 0000000000000..cbba981ae1b51 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php @@ -0,0 +1,280 @@ +\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; + + /** + * The mapping list of the lock name with the full lock path + * + * @var array + */ + private $locks = []; + + /** + * The default path to storage locks + */ + const DEFAULT_PATH = '/magento/locks'; + + /** + * @param string $host The host to connect to Zookeeper + * @param string $path The base path to locks in Zookeeper + * @throws RuntimeException + */ + public function __construct(string $host, string $path = self::DEFAULT_PATH) + { + if (!$path) { + throw new RuntimeException( + new Phrase('The path needs to be a non-empty string.') + ); + } + + if (!$host) { + throw new RuntimeException( + new Phrase('The host needs to be a non-empty string.') + ); + } + + $this->host = $host; + $this->path = rtrim($path, '/') . '/'; + } + + /** + * @inheritdoc + * + * You can see the lock algorithm by the link + * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + $skipDeadline = $timeout < 0; + $lockPath = $this->getFullPathToLock($name); + $deadline = microtime(true) + $timeout; + + if (!$this->checkAndCreateParentNode($lockPath)) { + throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); + } + + $lockKey = $this->getProvider() + ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); + + if (!$lockKey) { + throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); + } + + while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->getProvider()->delete($lockKey); + return false; + } + + usleep($this->sleepCycle); + } + + $this->locks[$name] = $lockKey; + + return true; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + if (!isset($this->locks[$name])) { + return false; + } + + return $this->getProvider()->delete($this->locks[$name]); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->isAnyLock($this->getFullPathToLock($name)); + } + + /** + * Gets full path to lock by its name + * + * @param string $name + * @return string + */ + private function getFullPathToLock(string $name): string + { + return $this->path . $name . '/' . $this->lockName; + } + + /** + * Initiolizes and returns Zookeeper provider + * + * @return \Zookeeper + * @throws RuntimeException + */ + private function getProvider(): \Zookeeper + { + if (!$this->zookeeper) { + $this->zookeeper = new \Zookeeper($this->host); + } + + $deadline = microtime(true) + $this->connectionTimeout; + while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { + if ($deadline <= microtime(true)) { + throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); + } + usleep($this->sleepCycle); + } + + return $this->zookeeper; + } + + /** + * Checks and creates base path recursively + * + * @param string $path + * @return bool + * @throws RuntimeException + */ + private function checkAndCreateParentNode(string $path): bool + { + $path = dirname($path); + if ($this->getProvider()->exists($path)) { + return true; + } + + if (!$this->checkAndCreateParentNode($path)) { + return false; + } + + if ($this->getProvider()->create($path, '1', $this->acl)) { + return true; + } + + return $this->getProvider()->exists($path); + } + + /** + * Gets int increment of lock key + * + * @param string $key + * @return int|null + */ + private function getIndex(string $key) + { + if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { + return null; + } + + return intval($matches[1]); + } + + /** + * Checks if there is any sequence node under parent of $fullKey. + * + * At first checks that the $fullKey node is present, if not - returns false. + * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, + * otherwise returns false. + * + * @param string $fullKey The full path without any sequence info + * @param int|null $indexKey The index to compare + * @return bool + * @throws RuntimeException + */ + private function isAnyLock(string $fullKey, int $indexKey = null): bool + { + $parent = dirname($fullKey); + + if (!$this->getProvider()->exists($parent)) { + return false; + } + + $children = $this->getProvider()->getChildren($parent); + + if (null === $indexKey && !empty($children)) { + return true; + } + + foreach ($children as $childKey) { + $childIndex = $this->getIndex($childKey); + + if (null === $childIndex) { + continue; + } + + if ($childIndex < $indexKey) { + return true; + } + } + + return false; + } +} diff --git a/lib/internal/Magento/Framework/Lock/LockBackendFactory.php b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php new file mode 100644 index 0000000000000..b142085ef6563 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php @@ -0,0 +1,111 @@ + DatabaseLock::class, + self::LOCK_ZOOKEEPER => ZookeeperLock::class, + self::LOCK_CACHE => CacheLock::class, + self::LOCK_FILE => FileLock::class, + ]; + + /** + * @param ObjectManagerInterface $objectManager The Object Manager instance + * @param DeploymentConfig $deploymentConfig The Application deployment configuration + */ + public function __construct( + ObjectManagerInterface $objectManager, + DeploymentConfig $deploymentConfig + ) { + $this->objectManager = $objectManager; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Creates an instance of LockManagerInterface using information from deployment config + * + * @return LockManagerInterface + * @throws RuntimeException + */ + public function create(): LockManagerInterface + { + $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); + $config = $this->deploymentConfig->get('lock/config', []); + + if (!isset($this->lockers[$provider])) { + throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); + } + + if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { + throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); + } + + return $this->objectManager->create($this->lockers[$provider], $config); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Proxy.php b/lib/internal/Magento/Framework/Lock/Proxy.php new file mode 100644 index 0000000000000..2718bf6cb3456 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Proxy.php @@ -0,0 +1,83 @@ +factory = $factory; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->getLocker()->isLocked($name); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->getLocker()->lock($name, $timeout); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + return $this->getLocker()->unlock($name); + } + + /** + * Gets LockManagerInterface implementation using Factory + * + * @return LockManagerInterface + * @throws RuntimeException + */ + private function getLocker(): LockManagerInterface + { + if (!$this->locker) { + $this->locker = $this->factory->create(); + } + + return $this->locker; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php index db3add62ff550..e2a95030bbd1c 100644 --- a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php @@ -7,8 +7,13 @@ namespace Magento\Framework\Lock\Test\Unit\Backend; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Lock\Backend\Database; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * @inheritdoc + */ class DatabaseTest extends \PHPUnit\Framework\TestCase { /** @@ -27,7 +32,7 @@ class DatabaseTest extends \PHPUnit\Framework\TestCase private $statement; /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + * @var ObjectManager */ private $objectManager; @@ -36,6 +41,14 @@ class DatabaseTest extends \PHPUnit\Framework\TestCase */ private $database; + /** + * @var DeploymentConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $deploymentConfig; + + /** + * @inheritdoc + */ protected function setUp() { $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) @@ -56,17 +69,32 @@ protected function setUp() ->method('query') ->willReturn($this->statement); - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->objectManager = new ObjectManager($this); + $this->deploymentConfig = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); /** @var Database $database */ $this->database = $this->objectManager->getObject( Database::class, - ['resource' => $this->resource] + [ + 'resource' => $this->resource, + 'deploymentConfig' => $this->deploymentConfig, + ] ); } + /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ public function testLock() { + $this->deploymentConfig + ->method('isDbAvailable') + ->with() + ->willReturn(true); $this->statement->expects($this->once()) ->method('fetchColumn') ->willReturn(true); @@ -75,18 +103,32 @@ public function testLock() } /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception * @expectedException \Magento\Framework\Exception\InputException */ public function testlockWithTooLongName() { + $this->deploymentConfig + ->method('isDbAvailable') + ->with() + ->willReturn(true); $this->database->lock('BbXbyf9rIY5xuAVdviQJmh76FyoeeVHTDpcjmcImNtgpO4Hnz4xk76ZGEyYALvrQu'); } /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception * @expectedException \Magento\Framework\Exception\AlreadyExistsException */ public function testlockWithAlreadyAcquiredLockInSameSession() { + $this->deploymentConfig + ->method('isDbAvailable') + ->with() + ->willReturn(true); $this->statement->expects($this->any()) ->method('fetchColumn') ->willReturn(true); @@ -94,4 +136,47 @@ public function testlockWithAlreadyAcquiredLockInSameSession() $this->database->lock('testLock'); $this->database->lock('differentLock'); } + + /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testLockWithUnavailableDeploymentConfig() + { + $this->deploymentConfig + ->expects($this->atLeast(1)) + ->method('isDbAvailable') + ->with() + ->willReturn(false); + $this->assertTrue($this->database->lock('testLock')); + } + + /** + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testUnlockWithUnavailableDeploymentConfig() + { + $this->deploymentConfig + ->expects($this->atLeast(1)) + ->method('isDbAvailable') + ->with() + ->willReturn(false); + $this->assertTrue($this->database->unlock('testLock')); + } + + /** + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testIsLockedWithUnavailableDB() + { + $this->deploymentConfig + ->expects($this->atLeast(1)) + ->method('isDbAvailable') + ->with() + ->willReturn(false); + $this->assertFalse($this->database->isLocked('testLock')); + } } diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php new file mode 100644 index 0000000000000..62521b9de3082 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php @@ -0,0 +1,68 @@ +markTestSkipped('Test was skipped because php extension Zookeeper is not installed.'); + } + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The path needs to be a non-empty string. + * @return void + */ + public function testConstructionWithPathException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, ''); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The host needs to be a non-empty string. + * @return void + */ + public function testConstructionWithHostException() + { + $this->zookeeperProvider = new ZookeeperProvider('', $this->path); + } + + /** + * @return void + */ + public function testConstructionWithoutException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, $this->path); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php new file mode 100644 index 0000000000000..ebf2f54f3e093 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php @@ -0,0 +1,116 @@ +objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->factory = new LockBackendFactory($this->objectManagerMock, $this->deploymentConfigMock); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage Unknown locks provider: someProvider + */ + public function testCreateWithException() + { + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls('someProvider', []); + + $this->factory->create(); + } + + /** + * @param string $lockProvider + * @param string $lockProviderClass + * @param array $config + * @dataProvider createDataProvider + */ + public function testCreate(string $lockProvider, string $lockProviderClass, array $config) + { + $lockManagerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls($lockProvider, $config); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with($lockProviderClass, $config) + ->willReturn($lockManagerMock); + + $this->assertSame($lockManagerMock, $this->factory->create()); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + $data = [ + 'db' => [ + 'lockProvider' => LockBackendFactory::LOCK_DB, + 'lockProviderClass' => DatabaseLock::class, + 'config' => ['prefix' => 'somePrefix'], + ], + 'cache' => [ + 'lockProvider' => LockBackendFactory::LOCK_CACHE, + 'lockProviderClass' => CacheLock::class, + 'config' => [], + ], + 'file' => [ + 'lockProvider' => LockBackendFactory::LOCK_FILE, + 'lockProviderClass' => FileLock::class, + 'config' => ['path' => '/my/path'], + ], + ]; + + if (extension_loaded('zookeeper')) { + $data['zookeeper'] = [ + 'lockProvider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'lockProviderClass' => ZookeeperLock::class, + 'config' => ['host' => 'some host'], + ]; + } + + return $data; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php new file mode 100644 index 0000000000000..c71dad701d715 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php @@ -0,0 +1,106 @@ +factoryMock = $this->createMock(LockBackendFactory::class); + $this->lockerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->proxy = new Proxy($this->factoryMock); + } + + /** + * @return void + */ + public function testIsLocked() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('isLocked') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->isLocked($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->isLocked($lockName)); + } + + /** + * @return void + */ + public function testLock() + { + $lockName = 'testLock'; + $timeout = 123; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('lock') + ->with($lockName, $timeout) + ->willReturn(true); + + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + } + + /** + * @return void + */ + public function testUnlock() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('unlock') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->unlock($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->unlock($lockName)); + } +} diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index a7bb96122a84d..b9271c0209fd3 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -177,29 +177,30 @@ public function setReplyTo($email, $name = null) /** * Set mail from address * - * @deprecated This function sets the from address for the first store only. - * new function setFromByStore introduced to allow setting of from address - * based on store. - * @see setFromByStore() + * @deprecated This function sets the from address but does not provide + * a way of setting the correct from addresses based on the scope. + * @see setFromByScope() * * @param string|array $from * @return $this + * @throws \Magento\Framework\Exception\MailException */ public function setFrom($from) { - return $this->setFromByStore($from, null); + return $this->setFromByScope($from, null); } /** - * Set mail from address by store + * Set mail from address by scopeId * * @param string|array $from - * @param string|int $store + * @param string|int $scopeId * @return $this + * @throws \Magento\Framework\Exception\MailException */ - public function setFromByStore($from, $store = null) + public function setFromByScope($from, $scopeId = null) { - $result = $this->_senderResolver->resolve($from, $store); + $result = $this->_senderResolver->resolve($from, $scopeId); $this->message->setFromAddress($result['email'], $result['name']); return $this; } @@ -256,6 +257,7 @@ public function setTemplateOptions($templateOptions) * Get mail transport * * @return \Magento\Framework\Mail\TransportInterface + * @throws LocalizedException */ public function getTransport() { diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php index b476eecd7f59f..5e3309af6497b 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php @@ -167,20 +167,20 @@ public function getTransportDataProvider() /** * @return void */ - public function testSetFromByStore() + public function testSetFromByScope() { $sender = ['email' => 'from@example.com', 'name' => 'name']; - $store = 1; + $scopeId = 1; $this->senderResolverMock->expects($this->once()) ->method('resolve') - ->with($sender, $store) + ->with($sender, $scopeId) ->willReturn($sender); $this->messageMock->expects($this->once()) ->method('setFromAddress') ->with($sender['email'], $sender['name']) ->willReturnSelf(); - $this->builder->setFromByStore($sender, $store); + $this->builder->setFromByScope($sender, $scopeId); } /** diff --git a/lib/internal/Magento/Framework/Message/Manager.php b/lib/internal/Magento/Framework/Message/Manager.php index c3b5701057d73..d71e196deea88 100644 --- a/lib/internal/Magento/Framework/Message/Manager.php +++ b/lib/internal/Magento/Framework/Message/Manager.php @@ -8,9 +8,12 @@ use Magento\Framework\Event; use Psr\Log\LoggerInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Debug; /** * Message manager model + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Manager implements ManagerInterface @@ -226,7 +229,7 @@ public function addUniqueMessages(array $messages, $group = null) $items = $this->getMessages(false, $group)->getItems(); foreach ($messages as $message) { - if ($message instanceof MessageInterface and !in_array($message, $items, false)) { + if ($message instanceof MessageInterface && !in_array($message, $items, false)) { $this->addMessage($message, $group); } } @@ -248,7 +251,12 @@ public function addException(\Exception $exception, $alternativeText = null, $gr 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); @@ -286,7 +294,12 @@ public function addExceptionMessage(\Exception $exception, $alternativeText = nu 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php index cb80bc4becaec..48c33c48f12e6 100644 --- a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php @@ -9,7 +9,7 @@ /** * Class CallbackInvoker to invoke callbacks for consumer classes */ -class CallbackInvoker +class CallbackInvoker implements CallbackInvokerInterface { /** * Run short running process diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php new file mode 100644 index 0000000000000..36658f2e4eebe --- /dev/null +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php @@ -0,0 +1,24 @@ +resource->getConnection()->rollBack(); } catch (\Exception $e) { + $retry = false; $this->resource->getConnection()->rollBack(); - $this->messageStatusProcessor->rejectMessages($queue, $messages); + if (strpos($e->getMessage(), 'Error while sending QUERY packet') !== false + && $this->retryCount < self::MAX_TRANSACTION_RETRIES + ) { + $retry = true; + $this->retryCount++; + $this->resource->closeConnection(); + $this->process($queue, $configuration, $messages, $messagesToAcknowledge, $mergedMessages); + } + if (!$retry) { + $this->messageStatusProcessor->rejectMessages($queue, $messages); + } } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php index 5e64d737034c8..73841a2a964cc 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php @@ -75,7 +75,7 @@ public function testProcess() ->getMockForAbstractClass(); $configuration->expects($this->atLeastOnce())->method('getHandlers')->willReturn([]); $this->messageStatusProcessor->expects($this->exactly(2))->method('acknowledgeMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) @@ -116,7 +116,7 @@ public function testProcessWithConnectionLostException() $exception = new \Magento\Framework\MessageQueue\ConnectionLostException(__('Exception Message')); $configuration->expects($this->atLeastOnce())->method('getHandlers')->willThrowException($exception); $this->messageStatusProcessor->expects($this->once())->method('acknowledgeMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) @@ -158,7 +158,7 @@ public function testProcessWithException() $configuration->expects($this->atLeastOnce())->method('getHandlers')->willThrowException($exception); $this->messageStatusProcessor->expects($this->once())->method('acknowledgeMessages'); $this->messageStatusProcessor->expects($this->atLeastOnce())->method('rejectMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index bdfb77762b41c..72421f793f131 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php +++ b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php @@ -126,16 +126,21 @@ private function getModuleConfigs() * * @param array $origList * @return array - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @throws \Exception */ - private function sortBySequence($origList) + private function sortBySequence(array $origList): array { ksort($origList); + $modules = $this->prearrangeModules($origList); + $expanded = []; - foreach ($origList as $moduleName => $value) { + foreach (array_keys($modules) as $moduleName) { + $sequence = $this->expandSequence($origList, $moduleName); + asort($sequence); + $expanded[] = [ 'name' => $moduleName, - 'sequence' => $this->expandSequence($origList, $moduleName), + 'sequence' => $sequence, ]; } @@ -143,7 +148,7 @@ private function sortBySequence($origList) $total = count($expanded); for ($i = 0; $i < $total - 1; $i++) { for ($j = $i; $j < $total; $j++) { - if (in_array($expanded[$j]['name'], $expanded[$i]['sequence'])) { + if (in_array($expanded[$j]['name'], $expanded[$i]['sequence'], true)) { $temp = $expanded[$i]; $expanded[$i] = $expanded[$j]; $expanded[$j] = $temp; @@ -159,6 +164,27 @@ private function sortBySequence($origList) return $result; } + /** + * Prearrange all modules by putting those from Magento before the others + * + * @param array $modules + * @return array + */ + private function prearrangeModules(array $modules): array + { + $breakdown = ['magento' => [], 'others' => []]; + + foreach ($modules as $moduleName => $moduleDetails) { + if (strpos($moduleName, 'Magento_') !== false) { + $breakdown['magento'][$moduleName] = $moduleDetails; + } else { + $breakdown['others'][$moduleName] = $moduleDetails; + } + } + + return array_merge($breakdown['magento'], $breakdown['others']); + } + /** * Accumulate information about all transitive "sequence" references * diff --git a/lib/internal/Magento/Framework/Module/PackageInfo.php b/lib/internal/Magento/Framework/Module/PackageInfo.php index 0dce507ba26f4..1eeb2bafc9623 100644 --- a/lib/internal/Magento/Framework/Module/PackageInfo.php +++ b/lib/internal/Magento/Framework/Module/PackageInfo.php @@ -8,8 +8,9 @@ use Magento\Framework\Component\ComponentRegistrar; /** - * Provide information of dependencies and conflicts in composer.json files, mapping of package name to module name, - * and mapping of module name to package version + * Provide information of dependencies and conflicts in composer.json files. + * + * Mapping of package name to module name, and mapping of module name to package version. */ class PackageInfo { @@ -176,8 +177,7 @@ public function getNonExistingDependencies() protected function convertPackageNameToModuleName($packageName) { $moduleName = str_replace('magento/module-', '', $packageName); - $moduleName = str_replace('-', ' ', $moduleName); - $moduleName = str_replace(' ', '', ucwords($moduleName)); + $moduleName = str_replace('-', '', ucwords($moduleName, '-')); return 'Magento_' . $moduleName; } diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php index fe613450fd485..a62bb5fa70f97 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php @@ -160,4 +160,55 @@ public function testLoadCircular() ])); $this->loader->load(); } + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testLoadPrearranged(): void + { + $fixtures = [ + 'Foo_Bar' => ['name' => 'Foo_Bar', 'sequence' => ['Magento_Store']], + 'Magento_Directory' => ['name' => 'Magento_Directory', 'sequence' => ['Magento_Store']], + 'Magento_Store' => ['name' => 'Magento_Store', 'sequence' => []], + 'Magento_Theme' => ['name' => 'Magento_Theme', 'sequence' => ['Magento_Store', 'Magento_Directory']], + 'Test_HelloWorld' => ['name' => 'Test_HelloWorld', 'sequence' => ['Magento_Theme']] + ]; + + $index = 0; + foreach ($fixtures as $name => $fixture) { + $this->converter->expects($this->at($index++))->method('convert')->willReturn([$name => $fixture]); + } + + $this->registry->expects($this->once()) + ->method('getPaths') + ->willReturn([ + '/path/to/Foo_Bar', + '/path/to/Magento_Directory', + '/path/to/Magento_Store', + '/path/to/Magento_Theme', + '/path/to/Test_HelloWorld' + ]); + + $this->driver->expects($this->exactly(5)) + ->method('fileGetContents') + ->will($this->returnValueMap([ + ['/path/to/Foo_Bar/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Magento_Directory/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Magento_Store/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Magento_Theme/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Test_HelloWorld/etc/module.xml', null, null, self::$sampleXml], + ])); + + // Load the full module list information + $result = $this->loader->load(); + + $this->assertSame( + ['Magento_Store', 'Magento_Directory', 'Magento_Theme', 'Foo_Bar', 'Test_HelloWorld'], + array_keys($result) + ); + + foreach ($fixtures as $name => $fixture) { + $this->assertSame($fixture, $result[$name]); + } + } } diff --git a/lib/internal/Magento/Framework/Oauth/Oauth.php b/lib/internal/Magento/Framework/Oauth/Oauth.php index 5e48fb5ed30f9..919b0e4c86ba0 100644 --- a/lib/internal/Magento/Framework/Oauth/Oauth.php +++ b/lib/internal/Magento/Framework/Oauth/Oauth.php @@ -9,6 +9,9 @@ use Magento\Framework\Encryption\Helper\Security; use Magento\Framework\Phrase; +/** + * Authorization service. + */ class Oauth implements OauthInterface { /** @@ -61,7 +64,7 @@ public static function getSupportedSignatureMethods() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRequestToken($params, $requestUrl, $httpMethod = 'POST') { @@ -74,7 +77,7 @@ public function getRequestToken($params, $requestUrl, $httpMethod = 'POST') } /** - * {@inheritdoc} + * @inheritdoc */ public function getAccessToken($params, $requestUrl, $httpMethod = 'POST') { @@ -102,7 +105,7 @@ public function getAccessToken($params, $requestUrl, $httpMethod = 'POST') } /** - * {@inheritdoc} + * @inheritdoc */ public function validateAccessTokenRequest($params, $requestUrl, $httpMethod = 'POST') { @@ -125,7 +128,7 @@ public function validateAccessTokenRequest($params, $requestUrl, $httpMethod = ' } /** - * {@inheritdoc} + * @inheritdoc */ public function validateAccessToken($accessToken) { @@ -133,7 +136,7 @@ public function validateAccessToken($accessToken) } /** - * {@inheritdoc} + * @inheritdoc */ public function buildAuthorizationHeader( $params, @@ -199,7 +202,7 @@ protected function _validateSignature($params, $consumerSecret, $httpMethod, $re ); if (!Security::compareStrings($calculatedSign, $params['oauth_signature'])) { - throw new Exception(new Phrase('The signatire is invalid. Verify and try again.')); + throw new Exception(new Phrase('The signature is invalid. Verify and try again.')); } } diff --git a/lib/internal/Magento/Framework/Option/ArrayPool.php b/lib/internal/Magento/Framework/Option/ArrayPool.php index 5ac349d99b82e..11e1b46ff0363 100644 --- a/lib/internal/Magento/Framework/Option/ArrayPool.php +++ b/lib/internal/Magento/Framework/Option/ArrayPool.php @@ -28,13 +28,14 @@ public function __construct(\Magento\Framework\ObjectManagerInterface $objectMan * * @param string $model * @throws \InvalidArgumentException - * @return \Magento\Framework\Option\ArrayInterface + * @return \Magento\Framework\Data\OptionSourceInterface */ public function get($model) { $modelInstance = $this->_objectManager->get($model); - if (false == $modelInstance instanceof \Magento\Framework\Option\ArrayInterface) { - throw new \InvalidArgumentException($model . 'doesn\'t implement \Magento\Framework\Option\ArrayInterface'); + if (false == $modelInstance instanceof \Magento\Framework\Data\OptionSourceInterface) { + throw new \InvalidArgumentException($model + . 'doesn\'t implement \Magento\Framework\Data\OptionSourceInterface'); } return $modelInstance; } diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne.php new file mode 100644 index 0000000000000..6382f4b247072 --- /dev/null +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne.php @@ -0,0 +1,13 @@ + 'addData', 'type' => 'array[]'], - ['method name' => 'addObjectList', 'type' => 'TSampleInterface[]'] + ['method name' => 'addObjectList', 'type' => '\\' . TSampleInterface::class . '[]'] ]; } @@ -354,4 +363,182 @@ public function testGetReturnTypeWithoutReturnTag() $methodReflection = $classReflection->getMethod('getName'); $this->typeProcessor->getGetterReturnType($methodReflection); } + + /** + * Simple and complex data provider + * + * @return array + */ + public function simpleAndComplexDataProvider(): array + { + return [ + ['string', true], + ['array', true], + ['int', true], + ['SomeClass', false], + ['\\My\\Namespace\\Model\\Class', false], + ['Some\\Other\\Class', false], + ]; + } + + /** + * Test simple type detection method + * + * @dataProvider simpleAndComplexDataProvider + * @param string $type + * @param bool $expectedValue + */ + public function testIsSimpleType(string $type, bool $expectedValue) + { + self::assertEquals($expectedValue, $this->typeProcessor->isSimpleType($type)); + } + + /** + * Simple and complex data provider + * + * @return array + */ + public function basicClassNameProvider(): array + { + return [ + ['SomeClass[]', 'SomeClass'], + ['\\My\\Namespace\\Model\\Class[]', '\\My\\Namespace\\Model\\Class'], + ['Some\\Other\\Class[]', 'Some\\Other\\Class'], + ['SomeClass', 'SomeClass'], + ['\\My\\Namespace\\Model\\Class', '\\My\\Namespace\\Model\\Class'], + ['Some\\Other\\Class', 'Some\\Other\\Class'], + ]; + } + + /** + * Extract basic class name + * + * @dataProvider basicClassNameProvider + * @param string $type + * @param string $expectedValue + */ + public function testBasicClassName(string $type, string $expectedValue) + { + self::assertEquals($expectedValue, $this->typeProcessor->getBasicClassName($type)); + } + + /** + * Fully qualified class names data provider + * + * @return array + */ + public function isFullyQualifiedClassNamesDataProvider(): array + { + return [ + ['SomeClass', false], + ['\\My\\Namespace\\Model\\Class', true], + ['Some\\Other\\Class', false], + ]; + } + + /** + * Test fully qualified class name detector + * + * @dataProvider isFullyQualifiedClassNamesDataProvider + * @param string $type + * @param bool $expectedValue + */ + public function testIsFullyQualifiedClassName(string $type, bool $expectedValue) + { + self::assertEquals($expectedValue, $this->typeProcessor->isFullyQualifiedClassName($type)); + } + + /** + * Test alias mapping + */ + public function testGetAliasMapping() + { + $sourceClass = new ClassReflection(UseSample::class); + $aliasMap = $this->typeProcessor->getAliasMapping($sourceClass); + + self::assertEquals([ + 'SampleOne' => SampleOne::class, + 'Sample2' => SampleTwo::class, + ], $aliasMap); + } + + /** + * Resolve fully qualified class names data provider + * + * @return array + */ + public function resolveFullyQualifiedClassNamesDataProvider(): array + { + return [ + [UseSample::class, 'string', 'string'], + [UseSample::class, 'string[]', 'string[]'], + + [UseSample::class, 'SampleOne', '\\' . SampleOne::class], + [UseSample::class, 'Sample2', '\\' . SampleTwo::class], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleOne', + '\\' . SampleOne::class + ], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleTwo', + '\\' . SampleTwo::class + ], + [UseSample::class, 'UseClasses\\SampleOne', '\\' . SampleOne::class], + [UseSample::class, 'UseClasses\\SampleTwo', '\\' . SampleTwo::class], + + [UseSample::class, 'SampleOne[]', '\\' . SampleOne::class . '[]'], + [UseSample::class, 'Sample2[]', '\\' . SampleTwo::class . '[]'], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleOne[]', + '\\' . SampleOne::class . '[]' + ], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleTwo[]', + '\\' . SampleTwo::class . '[]' + ], + [UseSample::class, 'UseClasses\\SampleOne[]', '\\' . SampleOne::class . '[]'], + [UseSample::class, 'UseClasses\\SampleTwo[]', '\\' . SampleTwo::class . '[]'], + + [UseSample::class, 'SampleOne\SampleThree', '\\' . SampleThree::class], + [UseSample::class, 'SampleOne\SampleThree[]', '\\' . SampleThree::class . '[]'], + + [UseSample::class, 'Sample2\SampleFour', '\\' . SampleFour::class], + [UseSample::class, 'Sample2\SampleFour[]', '\\' . SampleFour::class . '[]'], + + [UseSample::class, 'Sample2\NotExisting', 'Sample2\NotExisting'], + [UseSample::class, 'Sample2\NotExisting[]', 'Sample2\NotExisting[]'], + + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting', + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting' + ], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting[]', + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting[]' + ], + ]; + } + + /** + * Resolve fully qualified class names + * + * @dataProvider resolveFullyQualifiedClassNamesDataProvider + * @param string $className + * @param string $type + * @param string $expectedValue + * @throws \ReflectionException + */ + public function testResolveFullyQualifiedClassNames(string $className, string $type, string $expectedValue) + { + $sourceClass = new ClassReflection($className); + $fullyQualified = $this->typeProcessor->resolveFullyQualifiedClassName($sourceClass, $type); + + self::assertEquals($expectedValue, $fullyQualified); + } } diff --git a/lib/internal/Magento/Framework/Reflection/TypeProcessor.php b/lib/internal/Magento/Framework/Reflection/TypeProcessor.php index d7206032c68c7..9571fa53547ab 100644 --- a/lib/internal/Magento/Framework/Reflection/TypeProcessor.php +++ b/lib/internal/Magento/Framework/Reflection/TypeProcessor.php @@ -512,7 +512,7 @@ public function processSimpleAndAnyType($value, $type) public function getParamType(ParameterReflection $param) { $type = $param->detectType(); - if ($type == 'null') { + if ($type === 'null') { throw new \LogicException(sprintf( '@param annotation is incorrect for the parameter "%s" in the method "%s:%s".' . ' First declared type should not be null. E.g. string|null', @@ -521,14 +521,149 @@ public function getParamType(ParameterReflection $param) $param->getDeclaringFunction()->name )); } - if ($type == 'array') { + if ($type === 'array') { // try to determine class, if it's array of objects $paramDocBlock = $this->getParamDocBlockTag($param); $paramTypes = $paramDocBlock->getTypes(); $paramType = array_shift($paramTypes); + + $paramType = $this->resolveFullyQualifiedClassName($param->getDeclaringClass(), $paramType); + return strpos($paramType, '[]') !== false ? $paramType : "{$paramType}[]"; } - return $type; + + return $this->resolveFullyQualifiedClassName($param->getDeclaringClass(), $type); + } + + /** + * Get alias mapping for source class + * + * @param ClassReflection $sourceClass + * @return array + */ + public function getAliasMapping(ClassReflection $sourceClass): array + { + $sourceFileName = $sourceClass->getDeclaringFile(); + $aliases = []; + foreach ($sourceFileName->getUses() as $use) { + if ($use['as'] !== null) { + $aliases[$use['as']] = $use['use']; + } else { + $pos = strrpos($use['use'], '\\'); + + $aliasName = substr($use['use'], $pos + 1); + $aliases[$aliasName] = $use['use']; + } + } + + return $aliases; + } + + /** + * Return true if the passed type is a simple type + * + * Eg.: + * Return true with; array, string, ... + * Return false with: SomeClassName + * + * @param string $typeName + * @return bool + */ + public function isSimpleType(string $typeName): bool + { + return strtolower($typeName) === $typeName; + } + + /** + * Get basic type for a class name + * + * Eg.: + * SomeClassName[] => SomeClassName + * + * @param string $className + * @return string + */ + public function getBasicClassName(string $className): string + { + $pos = strpos($className, '['); + return ($pos === false) ? $className : substr($className, 0, $pos); + } + + /** + * Return true if it is a FQ class name + * + * Eg.: + * SomeClassName => false + * \My\NameSpace\SomeClassName => true + * + * @param string $className + * @return bool + */ + public function isFullyQualifiedClassName(string $className): bool + { + return strpos($className, '\\') === 0; + } + + /** + * Get aliased class name + * + * @param string $className + * @param string $namespace + * @param array $aliases + * @return string + */ + private function getAliasedClassName(string $className, string $namespace, array $aliases): string + { + $pos = strpos($className, '\\'); + if ($pos === false) { + $namespacePrefix = $className; + $partialClassName = ''; + } else { + $namespacePrefix = substr($className, 0, $pos); + $partialClassName = substr($className, $pos); + } + + if (isset($aliases[$namespacePrefix])) { + return $aliases[$namespacePrefix] . $partialClassName; + } + + return $namespace . '\\' . $className; + } + + /** + * Resolve fully qualified type name in the class alias context + * + * @param ClassReflection $sourceClass + * @param string $typeName + * @return string + */ + public function resolveFullyQualifiedClassName(ClassReflection $sourceClass, string $typeName): string + { + $typeName = trim($typeName); + + // Simple way to understand it is a basic type or a class name + if ($this->isSimpleType($typeName)) { + return $typeName; + } + + $basicTypeName = $this->getBasicClassName($typeName); + + // Already a FQN class name + if ($this->isFullyQualifiedClassName($basicTypeName)) { + return '\\' . substr($typeName, 1); + } + + $isArray = $this->isArrayType($typeName); + $aliases = $this->getAliasMapping($sourceClass); + + $namespace = $sourceClass->getNamespaceName(); + $fqClassName = '\\' . $this->getAliasedClassName($basicTypeName, $namespace, $aliases); + + if (interface_exists($fqClassName) || class_exists($fqClassName)) { + return $fqClassName . ($isArray ? '[]' : ''); + } + + return $typeName; } /** diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php index 897b67d8d46ec..e1b423d738a20 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php @@ -7,6 +7,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\DB\Select; use Magento\Framework\Search\Adapter\Mysql\Aggregation\Builder as AggregationBuilder; use Magento\Framework\Search\AdapterInterface; use Magento\Framework\Search\RequestInterface; @@ -49,6 +50,16 @@ class Adapter implements AdapterInterface */ private $temporaryStorageFactory; + /** + * Query Select Parts to be skipped when prepare query for count + * + * @var array + */ + private $countSqlSkipParts = [ + \Magento\Framework\DB\Select::LIMIT_COUNT => true, + \Magento\Framework\DB\Select::LIMIT_OFFSET => true, + ]; + /** * @param Mapper $mapper * @param ResponseFactory $responseFactory @@ -86,6 +97,7 @@ public function query(RequestInterface $request) $response = [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => $this->getSize($query) ]; return $this->responseFactory->create($response); } @@ -114,4 +126,39 @@ private function getConnection() { return $this->resource->getConnection(); } + + /** + * Get rows size + * + * @param Select $query + * @return int + */ + private function getSize(Select $query): int + { + $sql = $this->getSelectCountSql($query); + $parentSelect = $this->getConnection()->select(); + $parentSelect->from(['core_select' => $sql]); + $parentSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $parentSelect->columns('COUNT(*)'); + $totalRecords = $this->getConnection()->fetchOne($parentSelect); + + return intval($totalRecords); + } + + /** + * Reset limit and offset + * + * @param Select $query + * @return Select + */ + private function getSelectCountSql(Select $query): Select + { + foreach ($this->countSqlSkipParts as $part => $toSkip) { + if ($toSkip) { + $query->reset($part); + } + } + + return $query; + } } diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php index ea165e0cf9fcc..d4c98cda17e12 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Search\Adapter\Mysql\Query\Builder; use Magento\Framework\DB\Helper\Mysql\Fulltext; @@ -26,9 +28,9 @@ class Match implements QueryInterface /** * @var string */ - const SPECIAL_CHARACTERS = '+~/\\<>\'":*$#@()!,.?`=%&^'; + const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; - const MINIMAL_CHARACTER_LENGTH = 3; + const MINIMAL_CHARACTER_LENGTH = 1; /** * @var string[] diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php index 776dab93c2dcd..b41a43883fde5 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php @@ -68,7 +68,8 @@ public function create($rawResponse) \Magento\Framework\Search\Response\QueryResponse::class, [ 'documents' => $documents, - 'aggregations' => $aggregations + 'aggregations' => $aggregations, + 'total' => $rawResponse['total'] ] ); } diff --git a/lib/internal/Magento/Framework/Search/Request.php b/lib/internal/Magento/Framework/Search/Request.php index d55b2085e87ef..60f3338046613 100644 --- a/lib/internal/Magento/Framework/Search/Request.php +++ b/lib/internal/Magento/Framework/Search/Request.php @@ -54,6 +54,11 @@ class Request implements RequestInterface */ protected $dimensions; + /** + * @var array + */ + private $sort; + /** * @param string $name * @param string $indexName @@ -62,6 +67,7 @@ class Request implements RequestInterface * @param int|null $size * @param Dimension[] $dimensions * @param RequestBucketInterface[] $buckets + * @param array $sort */ public function __construct( $name, @@ -70,7 +76,8 @@ public function __construct( $from = null, $size = null, array $dimensions = [], - array $buckets = [] + array $buckets = [], + $sort = [] ) { $this->name = $name; $this->index = $indexName; @@ -79,10 +86,11 @@ public function __construct( $this->size = $size; $this->buckets = $buckets; $this->dimensions = $dimensions; + $this->sort = $sort; } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -90,7 +98,7 @@ public function getName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIndex() { @@ -98,7 +106,7 @@ public function getIndex() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDimensions() { @@ -106,7 +114,7 @@ public function getDimensions() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAggregation() { @@ -114,7 +122,7 @@ public function getAggregation() } /** - * {@inheritdoc} + * @inheritdoc */ public function getQuery() { @@ -122,7 +130,7 @@ public function getQuery() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFrom() { @@ -130,10 +138,18 @@ public function getFrom() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSize() { return $this->size; } + + /** + * @inheritdoc + */ + public function getSort() + { + return $this->sort; + } } diff --git a/lib/internal/Magento/Framework/Search/Request/Binder.php b/lib/internal/Magento/Framework/Search/Request/Binder.php index 34b5d72005e74..9df1ee87eb869 100644 --- a/lib/internal/Magento/Framework/Search/Request/Binder.php +++ b/lib/internal/Magento/Framework/Search/Request/Binder.php @@ -6,6 +6,8 @@ namespace Magento\Framework\Search\Request; /** + * Data binder for search request. + * * @api */ class Binder @@ -24,6 +26,9 @@ public function bind(array $requestData, array $bindData) $data['queries'] = $this->processData($requestData['queries'], $bindData['placeholder']); $data['filters'] = $this->processData($requestData['filters'], $bindData['placeholder']); $data['aggregations'] = $this->processData($requestData['aggregations'], $bindData['placeholder']); + if (isset($bindData['sort']) && isset($requestData['sort'])) { + $data['sort'] = $this->processData($requestData['sort'], $bindData['sort']); + } return $data; } @@ -48,6 +53,8 @@ private function processLimits($data, $bindData) } /** + * Dimensions process. + * * @param array $data * @param array $bindData * @return array diff --git a/lib/internal/Magento/Framework/Search/Request/Builder.php b/lib/internal/Magento/Framework/Search/Request/Builder.php index a16b3369cdda0..74bc65010a934 100644 --- a/lib/internal/Magento/Framework/Search/Request/Builder.php +++ b/lib/internal/Magento/Framework/Search/Request/Builder.php @@ -11,7 +11,10 @@ use Magento\Framework\Search\RequestInterface; /** + * Search request builder. + * * @api + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Builder { @@ -95,6 +98,18 @@ public function setFrom($from) return $this; } + /** + * Set sort. + * + * @param \Magento\Framework\Api\SortOrder[] $sort + * @return $this + */ + public function setSort($sort) + { + $this->data['sort'] = $sort; + return $this; + } + /** * Bind dimension data by name * @@ -139,6 +154,9 @@ public function create() } $data = $this->binder->bind($data, $this->data); + if (isset($this->data['sort'])) { + $data['sort'] = $this->prepareSorts($this->data['sort']); + } $data = $this->cleaner->clean($data); $this->clear(); @@ -146,6 +164,25 @@ public function create() return $this->convert($data); } + /** + * Prepare sort data for request. + * + * @param array $sorts + * @return array + */ + private function prepareSorts(array $sorts) + { + $sortData = []; + foreach ($sorts as $sortField => $direction) { + $sortData[] = [ + 'field' => $sortField, + 'direction' => $direction, + ]; + } + + return $sortData; + } + /** * Clear data * @@ -178,21 +215,27 @@ private function convert($data) 'filters' => $data['filters'] ] ); + $requestData = [ + 'name' => $data['query'], + 'indexName' => $data['index'], + 'from' => $data['from'], + 'size' => $data['size'], + 'query' => $mapper->getRootQuery(), + 'dimensions' => $this->buildDimensions(isset($data['dimensions']) ? $data['dimensions'] : []), + 'buckets' => $mapper->getBuckets() + ]; + if (isset($data['sort'])) { + $requestData['sort'] = $data['sort']; + } return $this->objectManager->create( \Magento\Framework\Search\Request::class, - [ - 'name' => $data['query'], - 'indexName' => $data['index'], - 'from' => $data['from'], - 'size' => $data['size'], - 'query' => $mapper->getRootQuery(), - 'dimensions' => $this->buildDimensions(isset($data['dimensions']) ? $data['dimensions'] : []), - 'buckets' => $mapper->getBuckets() - ] + $requestData ); } /** + * Build dimensions. + * * @param array $dimensionsData * @return array */ diff --git a/lib/internal/Magento/Framework/Search/RequestInterface.php b/lib/internal/Magento/Framework/Search/RequestInterface.php index 16df80f755c07..2de756e754a27 100644 --- a/lib/internal/Magento/Framework/Search/RequestInterface.php +++ b/lib/internal/Magento/Framework/Search/RequestInterface.php @@ -64,4 +64,11 @@ public function getFrom(); * @return int|null */ public function getSize(); + + /** + * Get Sort items + * + * @return array + */ + public function getSort(); } diff --git a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php index c2328398e53dc..90c7056ea2549 100644 --- a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php +++ b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php @@ -29,18 +29,26 @@ class QueryResponse implements ResponseInterface */ protected $aggregations; + /** + * @var int + */ + private $total; + /** * @param Document[] $documents * @param AggregationInterface $aggregations + * @param int $total */ - public function __construct(array $documents, AggregationInterface $aggregations) + public function __construct(array $documents, AggregationInterface $aggregations, int $total = 0) { $this->documents = $documents; $this->aggregations = $aggregations; + $this->total = $total; } /** - * Countable: return count of fields in document + * Countable: return count of fields in document. + * * @return int */ public function count() @@ -59,10 +67,18 @@ public function getIterator() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAggregations() { return $this->aggregations; } + + /** + * @inheritdoc + */ + public function getTotal(): int + { + return $this->total; + } } diff --git a/lib/internal/Magento/Framework/Search/ResponseInterface.php b/lib/internal/Magento/Framework/Search/ResponseInterface.php index 3b89528532602..c6c0d0ab59e10 100644 --- a/lib/internal/Magento/Framework/Search/ResponseInterface.php +++ b/lib/internal/Magento/Framework/Search/ResponseInterface.php @@ -16,4 +16,11 @@ interface ResponseInterface extends \IteratorAggregate, \Countable * @return \Magento\Framework\Api\Search\AggregationInterface */ public function getAggregations(); + + /** + * Return total count of items. + * + * @return int + */ + public function getTotal(): int; } diff --git a/lib/internal/Magento/Framework/Search/Search.php b/lib/internal/Magento/Framework/Search/Search.php index 16dfc6fdd1ddb..fe228546b55fb 100644 --- a/lib/internal/Magento/Framework/Search/Search.php +++ b/lib/internal/Magento/Framework/Search/Search.php @@ -10,6 +10,9 @@ use Magento\Framework\App\ScopeResolverInterface; use Magento\Framework\Search\Request\Builder; +/** + * Search API for all requests. + */ class Search implements SearchInterface { /** @@ -51,7 +54,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function search(SearchCriteriaInterface $searchCriteria) { @@ -68,6 +71,7 @@ public function search(SearchCriteriaInterface $searchCriteria) $this->requestBuilder->setFrom($searchCriteria->getCurrentPage() * $searchCriteria->getPageSize()); $this->requestBuilder->setSize($searchCriteria->getPageSize()); + $this->requestBuilder->setSort($searchCriteria->getSortOrders()); $request = $this->requestBuilder->create(); $searchResponse = $this->searchEngine->search($request); diff --git a/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php b/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php index 40fcd493861d7..2314252f4609c 100644 --- a/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php +++ b/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php @@ -9,6 +9,9 @@ use Magento\Framework\Api\Search\DocumentFactory; use Magento\Framework\Api\Search\SearchResultFactory; +/** + * Builder for search response. + */ class SearchResponseBuilder { /** @@ -35,6 +38,8 @@ public function __construct( } /** + * Build search result by search response. + * * @param ResponseInterface $response * @return SearchResultInterface */ @@ -46,7 +51,7 @@ public function build(ResponseInterface $response) $documents = iterator_to_array($response); $searchResult->setItems($documents); $searchResult->setAggregations($response->getAggregations()); - $searchResult->setTotalCount(count($documents)); + $searchResult->setTotalCount($response->getTotal()); return $searchResult; } diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php index 55d26493ca379..fbb56361bfe71 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php @@ -155,15 +155,22 @@ public function testQuery() 'aggregation2' => [2, 4], ], ], + 'total' => 1 ]; $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); - $this->connectionAdapter->expects($this->once()) + + $this->connectionAdapter->expects($this->exactly(2)) ->method('select') ->willReturn($select); + $this->connectionAdapter->expects($this->once()) + ->method('fetchOne') + ->with($select) + ->willReturn($selectResult['total']); + $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) ->disableOriginalConstructor() ->getMock(); diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php index c0699156decd0..3d21064b13d47 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/ResponseFactoryTest.php @@ -49,6 +49,7 @@ public function testCreate() ['title' => 'twoTitle', 'description' => 'twoDescription'], ], 'aggregations' => [], + 'total' => 2 ]; $this->documentFactory->expects($this->at(0))->method('create') @@ -61,7 +62,7 @@ public function testCreate() $this->objectManager->expects($this->once())->method('create') ->with( $this->equalTo(\Magento\Framework\Search\Response\QueryResponse::class), - $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => null]) + $this->equalTo(['documents' => ['document1', 'document2'], 'aggregations' => null, 'total' => 2]) ) ->will($this->returnValue('QueryResponseObject')); diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php index a60a280cb602a..a994559f28f0b 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Response/QueryResponseTest.php @@ -46,6 +46,7 @@ protected function setUp() [ 'documents' => $this->documents, 'aggregations' => $this->aggregations, + 'total' => 1 ] ); } diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php index a0b11959a42db..9ffafae9622f3 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/SearchResponseBuilderTest.php @@ -67,7 +67,7 @@ public function testBuild() /** @var QueryResponse|\PHPUnit_Framework_MockObject_MockObject $response */ $response = $this->getMockBuilder(\Magento\Framework\Search\Response\QueryResponse::class) - ->setMethods(['getIterator', 'getAggregations']) + ->setMethods(['getIterator', 'getAggregations', 'getTotal']) ->disableOriginalConstructor() ->getMockForAbstractClass(); $response->expects($this->any()) @@ -76,6 +76,9 @@ public function testBuild() $response->expects($this->once()) ->method('getAggregations') ->willReturn($aggregations); + $response->expects($this->any()) + ->method('getTotal') + ->willReturn(1); $result = $this->model->build($response); diff --git a/lib/internal/Magento/Framework/Search/etc/requests.xsd b/lib/internal/Magento/Framework/Search/etc/requests.xsd index a49124748b295..7d277382b698f 100644 --- a/lib/internal/Magento/Framework/Search/etc/requests.xsd +++ b/lib/internal/Magento/Framework/Search/etc/requests.xsd @@ -28,8 +28,8 @@ - - + + diff --git a/lib/internal/Magento/Framework/Serialize/README.md b/lib/internal/Magento/Framework/Serialize/README.md index 5af8fb7f71b6b..d900f89208a54 100644 --- a/lib/internal/Magento/Framework/Serialize/README.md +++ b/lib/internal/Magento/Framework/Serialize/README.md @@ -3,6 +3,7 @@ **Serialize** library provides interface *SerializerInterface* and multiple implementations: * *Json* - default implementation. Uses PHP native json_encode/json_decode functions; + * *JsonHexTag* - default implementation. Uses PHP native json_encode/json_decode functions with `JSON_HEX_TAG` option enabled; * *Serialize* - less secure than *Json*, but gives higher performance on big arrays. Uses PHP native serialize/unserialize functions, does not unserialize objects on PHP 7. Using *Serialize* implementation directly is discouraged, always use *SerializerInterface*, using *Serialize* implementation may lead to security vulnerabilities. \ No newline at end of file diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php index e352d0c2d7124..7ce9756ff243d 100644 --- a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php +++ b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php @@ -16,27 +16,27 @@ class Json implements SerializerInterface { /** - * {@inheritDoc} + * @inheritDoc * @since 100.2.0 */ public function serialize($data) { $result = json_encode($data); if (false === $result) { - throw new \InvalidArgumentException('Unable to serialize value.'); + throw new \InvalidArgumentException("Unable to serialize value. Error: " . json_last_error_msg()); } return $result; } /** - * {@inheritDoc} + * @inheritDoc * @since 100.2.0 */ public function unserialize($string) { $result = json_decode($string, true); if (json_last_error() !== JSON_ERROR_NONE) { - throw new \InvalidArgumentException('Unable to unserialize value.'); + throw new \InvalidArgumentException("Unable to unserialize value. Error: " . json_last_error_msg()); } return $result; } diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php new file mode 100644 index 0000000000000..4a5406ff3fd99 --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php @@ -0,0 +1,35 @@ + are converted to \u003C and \u003E), + * unserialize JSON encoded data + * + * @api + * @since 100.2.0 + */ +class JsonHexTag extends Json implements SerializerInterface +{ + /** + * @inheritDoc + * @since 100.2.0 + */ + public function serialize($data): string + { + $result = json_encode($data, JSON_HEX_TAG); + if (false === $result) { + throw new \InvalidArgumentException('Unable to serialize value.'); + } + return $result; + } +} diff --git a/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php new file mode 100644 index 0000000000000..c867dced0fc6e --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php @@ -0,0 +1,118 @@ +json = $objectManager->getObject(JsonHexTag::class); + } + + /** + * @param string|int|float|bool|array|null $value + * @param string $expected + * @dataProvider serializeDataProvider + */ + public function testSerialize($value, $expected) + { + $this->assertEquals( + $expected, + $this->json->serialize($value) + ); + } + + public function serializeDataProvider() + { + $dataObject = new DataObject(['something']); + return [ + ['', '""'], + ['string', '"string"'], + [null, 'null'], + [false, 'false'], + [['a' => 'b', 'd' => 123], '{"a":"b","d":123}'], + [123, '123'], + [10.56, '10.56'], + [$dataObject, '{}'], + ['< >', '"\u003C \u003E"'], + ]; + } + + /** + * @param string $value + * @param string|int|float|bool|array|null $expected + * @dataProvider unserializeDataProvider + */ + public function testUnserialize($value, $expected) + { + $this->assertEquals( + $expected, + $this->json->unserialize($value) + ); + } + + /** + * @return array + */ + public function unserializeDataProvider(): array + { + return [ + ['""', ''], + ['"string"', 'string'], + ['null', null], + ['false', false], + ['{"a":"b","d":123}', ['a' => 'b', 'd' => 123]], + ['123', 123], + ['10.56', 10.56], + ['{}', []], + ['"\u003C \u003E"', '< >'], + ]; + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to serialize value. + */ + public function testSerializeException() + { + $this->json->serialize(STDOUT); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to unserialize value. + * @dataProvider unserializeExceptionDataProvider + */ + public function testUnserializeException($value) + { + $this->json->unserialize($value); + } + + /** + * @return array + */ + public function unserializeExceptionDataProvider(): array + { + return [ + [''], + [false], + [null], + ['{'] + ]; + } +} diff --git a/lib/internal/Magento/Framework/Session/SessionManager.php b/lib/internal/Magento/Framework/Session/SessionManager.php index b53c83acb48cf..c7d201676b228 100644 --- a/lib/internal/Magento/Framework/Session/SessionManager.php +++ b/lib/internal/Magento/Framework/Session/SessionManager.php @@ -12,6 +12,7 @@ /** * Session Manager * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SessionManager implements SessionManagerInterface { @@ -36,7 +37,7 @@ class SessionManager implements SessionManagerInterface /** * Validator * - * @var \Magento\Framework\Session\ValidatorInterface + * @var ValidatorInterface */ protected $validator; @@ -50,28 +51,28 @@ class SessionManager implements SessionManagerInterface /** * SID resolver * - * @var \Magento\Framework\Session\SidResolverInterface + * @var SidResolverInterface */ protected $sidResolver; /** * Session config * - * @var \Magento\Framework\Session\Config\ConfigInterface + * @var Config\ConfigInterface */ protected $sessionConfig; /** * Save handler * - * @var \Magento\Framework\Session\SaveHandlerInterface + * @var SaveHandlerInterface */ protected $saveHandler; /** * Storage * - * @var \Magento\Framework\Session\StorageInterface + * @var StorageInterface */ protected $storage; @@ -92,6 +93,11 @@ class SessionManager implements SessionManagerInterface */ private $appState; + /** + * @var SessionStartChecker + */ + private $sessionStartChecker; + /** * @param \Magento\Framework\App\Request\Http $request * @param SidResolverInterface $sidResolver @@ -102,7 +108,10 @@ class SessionManager implements SessionManagerInterface * @param \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory * @param \Magento\Framework\App\State $appState + * @param SessionStartChecker|null $sessionStartChecker * @throws \Magento\Framework\Exception\SessionException + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Request\Http $request, @@ -113,7 +122,8 @@ public function __construct( StorageInterface $storage, \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager, \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory, - \Magento\Framework\App\State $appState + \Magento\Framework\App\State $appState, + SessionStartChecker $sessionStartChecker = null ) { $this->request = $request; $this->sidResolver = $sidResolver; @@ -124,11 +134,15 @@ public function __construct( $this->cookieManager = $cookieManager; $this->cookieMetadataFactory = $cookieMetadataFactory; $this->appState = $appState; + $this->sessionStartChecker = $sessionStartChecker ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + SessionStartChecker::class + ); $this->start(); } /** - * This method needs to support sessions with APC enabled + * This method needs to support sessions with APC enabled. + * * @return void */ public function writeClose() @@ -163,47 +177,49 @@ public function __call($method, $args) */ public function start() { - if (!$this->isSessionExists()) { - \Magento\Framework\Profiler::start('session_start'); - - try { - $this->appState->getAreaCode(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw new \Magento\Framework\Exception\SessionException( - new \Magento\Framework\Phrase( - 'Area code not set: Area code must be set before starting a session.' - ), - $e - ); - } + if ($this->sessionStartChecker->check()) { + if (!$this->isSessionExists()) { + \Magento\Framework\Profiler::start('session_start'); + + try { + $this->appState->getAreaCode(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new \Magento\Framework\Exception\SessionException( + new \Magento\Framework\Phrase( + 'Area code not set: Area code must be set before starting a session.' + ), + $e + ); + } - // Need to apply the config options so they can be ready by session_start - $this->initIniOptions(); - $this->registerSaveHandler(); - if (isset($_SESSION['new_session_id'])) { - // Not fully expired yet. Could be lost cookie by unstable network. - session_commit(); - session_id($_SESSION['new_session_id']); - } - $sid = $this->sidResolver->getSid($this); - // potential custom logic for session id (ex. switching between hosts) - $this->setSessionId($sid); - session_start(); - if (isset($_SESSION['destroyed']) - && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime() - ) { - $this->destroy(['clear_storage' => true]); - } + // Need to apply the config options so they can be ready by session_start + $this->initIniOptions(); + $this->registerSaveHandler(); + if (isset($_SESSION['new_session_id'])) { + // Not fully expired yet. Could be lost cookie by unstable network. + session_commit(); + session_id($_SESSION['new_session_id']); + } + $sid = $this->sidResolver->getSid($this); + // potential custom logic for session id (ex. switching between hosts) + $this->setSessionId($sid); + session_start(); + if (isset($_SESSION['destroyed']) + && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime() + ) { + $this->destroy(['clear_storage' => true]); + } - $this->validator->validate($this); - $this->renewCookie($sid); + $this->validator->validate($this); + $this->renewCookie($sid); - register_shutdown_function([$this, 'writeClose']); + register_shutdown_function([$this, 'writeClose']); - $this->_addHost(); - \Magento\Framework\Profiler::stop('session_start'); + $this->_addHost(); + \Magento\Framework\Profiler::stop('session_start'); + } + $this->storage->init(isset($_SESSION) ? $_SESSION : []); } - $this->storage->init(isset($_SESSION) ? $_SESSION : []); return $this; } diff --git a/lib/internal/Magento/Framework/Session/SessionStartChecker.php b/lib/internal/Magento/Framework/Session/SessionStartChecker.php new file mode 100644 index 0000000000000..9cc32268d574a --- /dev/null +++ b/lib/internal/Magento/Framework/Session/SessionStartChecker.php @@ -0,0 +1,38 @@ +checkSapi = $checkSapi; + } + + /** + * Can session be started or not. + * + * @return bool + */ + public function check() : bool + { + return !($this->checkSapi && PHP_SAPI === 'cli'); + } +} diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php index 34a99f26a4ef1..4c65d8a70bed5 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php @@ -350,6 +350,13 @@ private function processConstraints(array $tableData, string $resource, Schema $ if ($constraintData['type'] === 'foreign') { $constraintData['column'] = $this->getColumnByName($constraintData['column'], $table); $referenceTableData = $this->tablesData[$constraintData['referenceTable']]; + + if ($this->isDisabled($referenceTableData)) { + throw new \LogicException( + sprintf('The reference table named "%s" is disabled', $referenceTableData['name']) + ); + } + //If we are referenced to the same table we need to specify it //Get table name from resource connection regarding prefix settings $refTableName = $this->resourceConnection->getTableName($referenceTableData['name']); diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Index.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Index.php index 715f98c4177c0..211d3885297ba 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Index.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Index.php @@ -19,7 +19,7 @@ class Index implements FactoryInterface /** * Default index type. */ - const DEFAULT_INDEX_TYPE = "BTREE"; + const DEFAULT_INDEX_TYPE = "btree"; /** * @var ObjectManagerInterface diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd index 3eed77c37caac..c379452d65d85 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd @@ -9,7 +9,7 @@ - + diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd index a2f8611c4bd33..e3c54413f810b 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd @@ -84,6 +84,7 @@ + diff --git a/lib/internal/Magento/Framework/Setup/OldDbValidator.php b/lib/internal/Magento/Framework/Setup/OldDbValidator.php index 4c224a6c713ef..018b010e8fe4a 100644 --- a/lib/internal/Magento/Framework/Setup/OldDbValidator.php +++ b/lib/internal/Magento/Framework/Setup/OldDbValidator.php @@ -13,7 +13,7 @@ /** * Old Validator for database * - * Used in order to support backward compatability of modules that are installed + * Used in order to support backward compatibility of modules that are installed * in old way (with Install/Upgrade Schema/Data scripts) */ class OldDbValidator implements UpToDateValidatorInterface diff --git a/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php b/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php index abda94a0e6f8e..7573441fa9466 100644 --- a/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php +++ b/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Setup\Patch; /** - * Each patch can have dependecies, that should be applied before such patch + * Each patch can have dependencies, that should be applied before such patch * * / Patch2 --- Patch3 * / @@ -20,7 +20,7 @@ interface DependentPatchInterface /** * Get array of patches that have to be executed prior to this. * - * example of implementation: + * Example of implementation: * * [ * \Vendor_Name\Module_Name\Setup\Patch\Patch1::class, diff --git a/lib/internal/Magento/Framework/Setup/SchemaListener.php b/lib/internal/Magento/Framework/Setup/SchemaListener.php index c6407a2569a20..aabd7dedc911b 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaListener.php +++ b/lib/internal/Magento/Framework/Setup/SchemaListener.php @@ -12,6 +12,9 @@ /** * Listen for all changes and record them in order to reuse later. + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class SchemaListener { @@ -61,7 +64,8 @@ class SchemaListener 'SCALE' => 'scale', 'UNSIGNED' => 'unsigned', 'IDENTITY' => 'identity', - 'PRIMARY' => 'primary' + 'PRIMARY' => 'primary', + 'COMMENT' => 'comment', ]; /** @@ -71,7 +75,6 @@ class SchemaListener 'COLUMN_POSITION', 'COLUMN_TYPE', 'PRIMARY_POSITION', - 'COMMENT' ]; /** @@ -90,8 +93,6 @@ class SchemaListener private $handlers; /** - * Constructor. - * * @param array $definitionMappers * @param array $handlers */ @@ -132,7 +133,9 @@ private function castColumnDefinition($definition, $columnName) $definition = $this->doColumnMapping($definition); $definition['name'] = strtolower($columnName); $definitionType = $definition['type'] === 'int' ? 'integer' : $definition['type']; + $columnComment = $definition['comment'] ?? null; $definition = $this->definitionMappers[$definitionType]->convertToDefinition($definition); + $definition['comment'] = $columnComment; if (isset($definition['default']) && $definition['default'] === false) { $definition['default'] = null; //uniform default values } @@ -214,7 +217,7 @@ private function doColumnMapping(array $definition) * @param string $columnName * @param array $definition * @param string $primaryKeyName - * @param null $onCreate + * @param string|null $onCreate */ public function addColumn($tableName, $columnName, $definition, $primaryKeyName = 'PRIMARY', $onCreate = null) { @@ -448,6 +451,7 @@ private function prepareColumns($tableName, array $tableColumns) * @param array $foreignKeys * @param array $indexes * @param string $tableName + * @param string $engine */ private function prepareConstraintsAndIndexes(array $foreignKeys, array $indexes, $tableName, $engine) { @@ -478,11 +482,16 @@ private function prepareConstraintsAndIndexes(array $foreignKeys, array $indexes * Create table. * * @param Table $table + * @throws \Zend_Db_Exception */ public function createTable(Table $table) { $engine = strtolower($table->getOption('type')); - $this->tables[$this->getModuleName()][strtolower($table->getName())]['engine'] = $engine; + $this->tables[$this->getModuleName()][strtolower($table->getName())] = + [ + 'engine' => $engine, + 'comment' => $table->getComment(), + ]; $this->prepareColumns($table->getName(), $table->getColumns()); $this->prepareConstraintsAndIndexes($table->getForeignKeys(), $table->getIndexes(), $table->getName(), $engine); } @@ -510,7 +519,7 @@ public function toogleIgnore($flag) /** * Drop table. * - * @param $tableName + * @param string $tableName */ public function dropTable($tableName) { diff --git a/lib/internal/Magento/Framework/Setup/SchemaPersistor.php b/lib/internal/Magento/Framework/Setup/SchemaPersistor.php index f3af56b8ac2ca..51f61f1dde13b 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaPersistor.php +++ b/lib/internal/Magento/Framework/Setup/SchemaPersistor.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Setup; use Magento\Framework\Component\ComponentRegistrar; -use Magento\Framework\Shell; +use Magento\Framework\Setup\Declaration\Schema\Sharding; /** * Persist listened schema to db_schema.xml file. @@ -24,8 +24,6 @@ class SchemaPersistor private $xmlPersistor; /** - * Constructor. - * * @param ComponentRegistrar $componentRegistrar * @param XmlPersistor $xmlPersistor */ @@ -64,32 +62,88 @@ public function persist(SchemaListener $schemaListener) continue; } $schemaPatch = sprintf('%s/etc/db_schema.xml', $path); - if (file_exists($schemaPatch)) { - $dom = new \SimpleXMLElement(file_get_contents($schemaPatch)); - } else { - $dom = $this->initEmptyDom(); - } + $dom = $this->processTables($schemaPatch, $tablesData); + $this->persistModule($dom, $schemaPatch); + } + } + + /** + * Convert tables data into XML document. + * + * @param string $schemaPatch + * @param array $tablesData + * @return \SimpleXMLElement + */ + private function processTables(string $schemaPatch, array $tablesData): \SimpleXMLElement + { + if (file_exists($schemaPatch)) { + $dom = new \SimpleXMLElement(file_get_contents($schemaPatch)); + } else { + $dom = $this->initEmptyDom(); + } + $defaultAttributesValues = [ + 'resource' => Sharding::DEFAULT_CONNECTION, + ]; - foreach ($tablesData as $tableName => $tableData) { - $tableData = $this->handleDefinition($tableData); + foreach ($tablesData as $tableName => $tableData) { + $tableData = $this->handleDefinition($tableData); + $table = $dom->xpath("//table[@name='" . $tableName . "']"); + if (!$table) { $table = $dom->addChild('table'); $table->addAttribute('name', $tableName); - $table->addAttribute('resource', $tableData['resource']); - if (isset($tableData['engine']) && $tableData['engine'] !== null) { - $table->addAttribute('engine', $tableData['engine']); - } + } else { + $table = reset($table); + } - $this->processColumns($tableData, $table); - $this->processConstraints($tableData, $table); - $this->processIndexes($tableData, $table); + $attributeNames = ['disabled', 'resource', 'engine', 'comment']; + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $table, + $attributeName, + $tableData, + $defaultAttributesValues[$attributeName] ?? null + ); } - $this->persistModule($dom, $schemaPatch); + $this->processColumns($tableData, $table); + $this->processConstraints($tableData, $table); + $this->processIndexes($tableData, $table); + } + + return $dom; + } + + /** + * Update element attribute value or create new attribute. + * + * @param \SimpleXMLElement $element + * @param string $attributeName + * @param array $elementData + * @param string|null $defaultValue + */ + private function updateElementAttribute( + \SimpleXMLElement $element, + string $attributeName, + array $elementData, + ?string $defaultValue = null + ) { + $attributeValue = $elementData[$attributeName] ?? $defaultValue; + if ($attributeValue !== null) { + if (is_bool($attributeValue)) { + $attributeValue = $this->castBooleanToString($attributeValue); + } + + if ($element->attributes()[$attributeName]) { + $element->attributes()->$attributeName = $attributeValue; + } else { + $element->addAttribute($attributeName, $attributeValue); + } } } /** * If disabled attribute is set to false it remove it at all. + * * Also handle other generic attributes. * * @param array $definition @@ -124,24 +178,30 @@ private function castBooleanToString($boolean) */ private function processColumns(array $tableData, \SimpleXMLElement $table) { - if (isset($tableData['columns'])) { - foreach ($tableData['columns'] as $columnData) { - $columnData = $this->handleDefinition($columnData); - $domColumn = $table->addChild('column'); - $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi'); - unset($columnData['xsi:type']); - - foreach ($columnData as $attributeKey => $attributeValue) { - if ($attributeValue === null) { - continue; - } - - if (is_bool($attributeValue)) { - $attributeValue = $this->castBooleanToString($attributeValue); - } + if (!isset($tableData['columns'])) { + return $table; + } - $domColumn->addAttribute($attributeKey, $attributeValue); + foreach ($tableData['columns'] as $columnName => $columnData) { + $columnData = $this->handleDefinition($columnData); + $domColumn = $table->xpath("column[@name='" . $columnName . "']"); + if (!$domColumn) { + $domColumn = $table->addChild('column'); + if (!empty($columnData['xsi:type'])) { + $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi'); } + $domColumn->addAttribute('name', $columnName); + } else { + $domColumn = reset($domColumn); + } + + $attributeNames = array_diff(array_keys($columnData), ['name', 'xsi:type']); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domColumn, + $attributeName, + $columnData + ); } } @@ -160,14 +220,29 @@ private function processIndexes(array $tableData, \SimpleXMLElement $table) if (isset($tableData['indexes'])) { foreach ($tableData['indexes'] as $indexName => $indexData) { $indexData = $this->handleDefinition($indexData); - $domIndex = $table->addChild('index'); - $domIndex->addAttribute('name', $indexName); - if (isset($indexData['disabled']) && $indexData['disabled']) { - $domIndex->addAttribute('disabled', true); - } else { - $domIndex->addAttribute('indexType', $indexData['indexType']); + $domIndex = $table->xpath("index[@referenceId='" . $indexName . "']"); + if (!$domIndex) { + $domIndex = $this->getUniqueIndexByName($table, $indexName); + } + + if (!$domIndex) { + $domIndex = $table->addChild('index'); + $domIndex->addAttribute('referenceId', $indexName); + } elseif (is_array($domIndex)) { + $domIndex = reset($domIndex); + } + $attributeNames = array_diff(array_keys($indexData), ['referenceId', 'columns', 'name']); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domIndex, + $attributeName, + $indexData + ); + } + + if (!empty($indexData['columns'])) { foreach ($indexData['columns'] as $column) { $columnXml = $domIndex->addChild('column'); $columnXml->addAttribute('name', $column); @@ -188,37 +263,48 @@ private function processIndexes(array $tableData, \SimpleXMLElement $table) */ private function processConstraints(array $tableData, \SimpleXMLElement $table) { - if (isset($tableData['constraints'])) { - foreach ($tableData['constraints'] as $constraintType => $constraints) { - if ($constraintType === 'foreign') { - foreach ($constraints as $name => $constraintData) { - $constraintData = $this->handleDefinition($constraintData); - $constraintDom = $table->addChild('constraint'); - $constraintDom->addAttribute('xsi:type', $constraintType, 'xsi'); - $constraintDom->addAttribute('name', $name); - - foreach ($constraintData as $attributeKey => $attributeValue) { - $constraintDom->addAttribute($attributeKey, $attributeValue); - } - } + if (!isset($tableData['constraints'])) { + return $table; + } + + foreach ($tableData['constraints'] as $constraintType => $constraints) { + foreach ($constraints as $constraintName => $constraintData) { + $constraintData = $this->handleDefinition($constraintData); + $domConstraint = $table->xpath("constraint[@referenceId='" . $constraintName . "']"); + if (!$domConstraint) { + $domConstraint = $table->addChild('constraint'); + $domConstraint->addAttribute('xsi:type', $constraintType, 'xsi'); + $domConstraint->addAttribute('referenceId', $constraintName); } else { - foreach ($constraints as $name => $constraintData) { - $constraintData = $this->handleDefinition($constraintData); - $constraintDom = $table->addChild('constraint'); - $constraintDom->addAttribute('xsi:type', $constraintType, 'xsi'); - $constraintDom->addAttribute('name', $name); - $constraintData['columns'] = $constraintData['columns'] ?? []; - - if (isset($constraintData['disabled'])) { - $constraintDom->addAttribute('disabled', (bool) $constraintData['disabled']); - } - - foreach ($constraintData['columns'] as $column) { - $columnXml = $constraintDom->addChild('column'); - $columnXml->addAttribute('name', $column); - } + $domConstraint = reset($domConstraint); + } + + $attributeNames = array_diff( + array_keys($constraintData), + ['referenceId', 'xsi:type', 'disabled', 'columns', 'name', 'type'] + ); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domConstraint, + $attributeName, + $constraintData + ); + } + + if (!empty($constraintData['columns'])) { + foreach ($constraintData['columns'] as $column) { + $columnXml = $domConstraint->addChild('column'); + $columnXml->addAttribute('name', $column); } } + + if (!empty($constraintData['disabled'])) { + $this->updateElementAttribute( + $domConstraint, + 'disabled', + $constraintData + ); + } } } @@ -236,4 +322,26 @@ private function persistModule(\SimpleXMLElement $simpleXmlElementDom, $path) { $this->xmlPersistor->persist($simpleXmlElementDom, $path); } + + /** + * Retrieve unique index declaration by name. + * + * @param \SimpleXMLElement $table + * @param string $indexName + * @return \SimpleXMLElement|null + */ + private function getUniqueIndexByName(\SimpleXMLElement $table, string $indexName): ?\SimpleXMLElement + { + $indexElement = null; + $constraint = $table->xpath("constraint[@referenceId='" . $indexName . "']"); + if ($constraint) { + $constraint = reset($constraint); + $type = $constraint->attributes('xsi', true)->type; + if ($type == 'unique') { + $indexElement = $constraint; + } + } + + return $indexElement; + } } diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php index f89bdc9e137dd..cb40845bcc488 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php @@ -91,7 +91,7 @@ class PatchApplierTest extends \PHPUnit\Framework\TestCase /** * @var PatchBackwardCompatability |\PHPUnit_Framework_MockObject_MockObject */ - private $patchBacwardCompatability; + private $patchBackwardCompatability; protected function setUp() { @@ -109,7 +109,7 @@ protected function setUp() $this->moduleDataSetupMock->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); $objectManager = new ObjectManager($this); - $this->patchBacwardCompatability = $objectManager->getObject( + $this->patchBackwardCompatability = $objectManager->getObject( PatchBackwardCompatability::class, [ 'moduleResource' => $this->moduleResourceMock @@ -128,7 +128,7 @@ protected function setUp() 'objectManager' => $this->objectManagerMock, 'schemaSetup' => $this->schemaSetupMock, 'moduleDataSetup' => $this->moduleDataSetupMock, - 'patchBackwardCompatability' => $this->patchBacwardCompatability + 'patchBackwardCompatability' => $this->patchBackwardCompatability ] ); require_once __DIR__ . '/../_files/data_patch_classes.php'; diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php index 4e34b3aebbf3e..cfde80b12ee3a 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php @@ -127,6 +127,7 @@ public function testCreateTable() : void 'default' => 'CURRENT_TIMESTAMP', 'disabled' => false, 'onCreate' => null, + 'comment' => 'Column with type timestamp init update', ], 'integer' => [ @@ -139,6 +140,7 @@ public function testCreateTable() : void 'default' => null, 'disabled' => false, 'onCreate' => null, + 'comment' => 'Integer' ], 'decimal' => [ @@ -151,6 +153,7 @@ public function testCreateTable() : void 'default' => null, 'disabled' => false, 'onCreate' => null, + 'comment' => 'Decimal' ], ], $tables['First_Module']['new_table']['columns'] diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php index cc88af15a262b..f65e6c910dc0d 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php @@ -152,13 +152,13 @@ public function schemaListenerTablesDataProvider() : array - - + - + diff --git a/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php b/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php index 75f8b1cd0633f..913d5a577e62a 100644 --- a/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php @@ -57,6 +57,27 @@ public function testWalk() ); } + /** + * Ensure that getSize works correctly with clear + * + */ + public function testClearTotalRecords() + { + $objOne = new \Magento\Framework\DataObject(['id' => 1, 'name' => 'one']); + $objTwo = new \Magento\Framework\DataObject(['id' => 2, 'name' => 'two']); + $objThree = new \Magento\Framework\DataObject(['id' => 3, 'name' => 'three']); + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->collection->addItem($objOne); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->collection->addItem($objTwo); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->collection->addItem($objThree); + $this->assertEquals(3, $this->collection->getSize()); + $this->collection->clear(); + $this->assertEquals(0, $this->collection->getSize()); + } + /** * Callback function. * diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index 1599cae073f30..e406994b54c17 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -30,9 +30,12 @@ class EscaperTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $this->escaper = new Escaper(); $this->zendEscaper = new \Magento\Framework\ZendEscaper(); $this->loggerMock = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class); - $this->escaper = new Escaper($this->zendEscaper, $this->loggerMock); + $objectManagerHelper = new ObjectManager($this); + $objectManagerHelper->setBackwardCompatibleProperty($this->escaper, 'escaper', $this->zendEscaper); + $objectManagerHelper->setBackwardCompatibleProperty($this->escaper, 'logger', $this->loggerMock); } /** diff --git a/lib/internal/Magento/Framework/Test/Unit/UrlTest.php b/lib/internal/Magento/Framework/Test/Unit/UrlTest.php index 939a9c484432a..046a9d63fc8f4 100644 --- a/lib/internal/Magento/Framework/Test/Unit/UrlTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/UrlTest.php @@ -141,7 +141,7 @@ protected function getUrlModel($arguments = []) $modelProperty->setValue($model, $this->urlModifier); $zendEscaper = new \Magento\Framework\ZendEscaper(); - $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $escaper = new \Magento\Framework\Escaper(); $objectManager->setBackwardCompatibleProperty($escaper, 'escaper', $zendEscaper); $objectManager->setBackwardCompatibleProperty($model, 'escaper', $escaper); diff --git a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php index 1e8eb74bf5414..315c3ee204864 100644 --- a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php +++ b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php @@ -6,6 +6,8 @@ namespace Magento\Framework\View\Asset\MergeStrategy; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; use Magento\Framework\View\Asset; /** @@ -30,38 +32,46 @@ class Direct implements \Magento\Framework\View\Asset\MergeStrategyInterface */ private $cssUrlResolver; + /** + * @var Random + */ + private $mathRandom; + /** * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\View\Url\CssResolver $cssUrlResolver + * @param Random|null $mathRandom */ public function __construct( \Magento\Framework\Filesystem $filesystem, - \Magento\Framework\View\Url\CssResolver $cssUrlResolver + \Magento\Framework\View\Url\CssResolver $cssUrlResolver, + Random $mathRandom = null ) { $this->filesystem = $filesystem; $this->cssUrlResolver = $cssUrlResolver; + $this->mathRandom = $mathRandom ?: ObjectManager::getInstance()->get(Random::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function merge(array $assetsToMerge, Asset\LocalInterface $resultAsset) { $mergedContent = $this->composeMergedContent($assetsToMerge, $resultAsset); $filePath = $resultAsset->getPath(); + $tmpFilePath = $filePath . $this->mathRandom->getUniqueHash('_'); $staticDir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); $tmpDir = $this->filesystem->getDirectoryWrite(DirectoryList::TMP); - $tmpDir->writeFile($filePath, $mergedContent); - $tmpDir->renameFile($filePath, $filePath, $staticDir); + $tmpDir->writeFile($tmpFilePath, $mergedContent); + $tmpDir->renameFile($tmpFilePath, $filePath, $staticDir); } /** * Merge files together and modify content if needed * - * @param \Magento\Framework\View\Asset\MergeableInterface[] $assetsToMerge - * @param \Magento\Framework\View\Asset\LocalInterface $resultAsset - * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @param array $assetsToMerge + * @param Asset\LocalInterface $resultAsset + * @return array|string */ private function composeMergedContent(array $assetsToMerge, Asset\LocalInterface $resultAsset) { diff --git a/lib/internal/Magento/Framework/View/Asset/Repository.php b/lib/internal/Magento/Framework/View/Asset/Repository.php index 654d80382f4b0..19c9ddd1e9186 100644 --- a/lib/internal/Magento/Framework/View/Asset/Repository.php +++ b/lib/internal/Magento/Framework/View/Asset/Repository.php @@ -126,6 +126,7 @@ public function __construct( * @return $this * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function updateDesignParams(array &$params) { @@ -146,7 +147,12 @@ public function updateDesignParams(array &$params) } if ($theme) { - $params['themeModel'] = $this->getThemeProvider()->getThemeByFullPath($area . '/' . $theme); + if (is_numeric($theme)) { + $params['themeModel'] = $this->getThemeProvider()->getThemeById($theme); + } else { + $params['themeModel'] = $this->getThemeProvider()->getThemeByFullPath($area . '/' . $theme); + } + if (!$params['themeModel']) { throw new \UnexpectedValueException("Could not find theme '$theme' for area '$area'"); } @@ -167,6 +173,8 @@ public function updateDesignParams(array &$params) } /** + * Get theme provider + * * @return ThemeProviderInterface */ private function getThemeProvider() @@ -440,6 +448,8 @@ public static function extractModule($fileId) } /** + * Get repository files map + * * @param string $fileId * @param array $params * @return RepositoryMap diff --git a/lib/internal/Magento/Framework/View/Context.php b/lib/internal/Magento/Framework/View/Context.php index c3f1c3e691c84..508d63d158bd7 100644 --- a/lib/internal/Magento/Framework/View/Context.php +++ b/lib/internal/Magento/Framework/View/Context.php @@ -14,6 +14,7 @@ use Magento\Framework\Event\ManagerInterface; use Psr\Log\LoggerInterface as Logger; use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\TranslateInterface; use Magento\Framework\UrlInterface; use Magento\Framework\View\ConfigInterface as ViewConfig; @@ -144,6 +145,7 @@ class Context * @param Logger $logger * @param AppState $appState * @param LayoutInterface $layout + * @param SessionManagerInterface|null $sessionManager * * @todo reduce parameter number * @@ -163,7 +165,8 @@ public function __construct( CacheState $cacheState, Logger $logger, AppState $appState, - LayoutInterface $layout + LayoutInterface $layout, + SessionManagerInterface $sessionManager = null ) { $this->request = $request; $this->eventManager = $eventManager; @@ -171,7 +174,7 @@ public function __construct( $this->translator = $translator; $this->cache = $cache; $this->design = $design; - $this->session = $session; + $this->session = $sessionManager ?: $session; $this->scopeConfig = $scopeConfig; $this->frontController = $frontController; $this->viewConfig = $viewConfig; @@ -332,6 +335,8 @@ public function getModuleName() } /** + * Get Front Name + * * @see getModuleName */ public function getFrontName() diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index 335006555d2f1..6c4746d8218ea 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\View\Element; +use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\App\ObjectManager; /** * Base class for all blocks. @@ -175,14 +178,23 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl */ protected $_cache; + /** + * @var LockGuardedCacheLoader + */ + private $lockQuery; + /** * Constructor * * @param \Magento\Framework\View\Element\Context $context * @param array $data + * @param LockGuardedCacheLoader|null $lockQuery */ - public function __construct(\Magento\Framework\View\Element\Context $context, array $data = []) - { + public function __construct( + \Magento\Framework\View\Element\Context $context, + array $data = [], + LockGuardedCacheLoader $lockQuery = null + ) { $this->_request = $context->getRequest(); $this->_layout = $context->getLayout(); $this->_eventManager = $context->getEventManager(); @@ -204,6 +216,8 @@ public function __construct(\Magento\Framework\View\Element\Context $context, ar $this->jsLayout = $data['jsLayout']; unset($data['jsLayout']); } + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); parent::__construct($data); $this->_construct(); } @@ -658,19 +672,6 @@ public function toHtml() } $html = $this->_loadCache(); - if ($html === false) { - if ($this->hasData('translate_inline')) { - $this->inlineTranslation->suspend($this->getData('translate_inline')); - } - - $this->_beforeToHtml(); - $html = $this->_toHtml(); - $this->_saveCache($html); - - if ($this->hasData('translate_inline')) { - $this->inlineTranslation->resume(); - } - } $html = $this->_afterToHtml($html); /** @var \Magento\Framework\DataObject */ @@ -1083,23 +1084,54 @@ protected function getCacheLifetime() /** * Load block html from cache storage * - * @return string|false + * @return string */ protected function _loadCache() { + $collectAction = function () { + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->suspend($this->getData('translate_inline')); + } + + $this->_beforeToHtml(); + return $this->_toHtml(); + }; + if ($this->getCacheLifetime() === null || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) { - return false; - } - $cacheKey = $this->getCacheKey(); - $cacheData = $this->_cache->load($cacheKey); - if ($cacheData) { - $cacheData = str_replace( - $this->_getSidPlaceholder($cacheKey), - $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), - $cacheData - ); + $html = $collectAction(); + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->resume(); + } + return $html; } - return $cacheData; + $loadAction = function () { + $cacheKey = $this->getCacheKey(); + $cacheData = $this->_cache->load($cacheKey); + if ($cacheData) { + $cacheData = str_replace( + $this->_getSidPlaceholder($cacheKey), + $this->_sidResolver->getSessionIdQueryParam($this->_session) + . '=' + . $this->_session->getSessionId(), + $cacheData + ); + } + return $cacheData; + }; + + $saveAction = function ($data) { + $this->_saveCache($data); + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->resume(); + } + }; + + return (string)$this->lockQuery->lockedLoadData( + $this->getCacheKey(), + $loadAction, + $collectAction, + $saveAction + ); } /** diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/Context.php b/lib/internal/Magento/Framework/View/Element/UiComponent/Context.php index e472a9c9effb1..fbb84712b2afd 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/Context.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/Context.php @@ -5,7 +5,9 @@ */ namespace Magento\Framework\View\Element\UiComponent; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\UiComponent\ContentType\ContentTypeFactory; use Magento\Framework\View\Element\UiComponent\Control\ActionPoolFactory; @@ -94,6 +96,11 @@ class Context implements ContextInterface */ protected $uiComponentFactory; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param PageLayoutInterface $pageLayout * @param RequestInterface $request @@ -104,7 +111,8 @@ class Context implements ContextInterface * @param Processor $processor * @param UiComponentFactory $uiComponentFactory * @param DataProviderInterface|null $dataProvider - * @param string|null $namespace + * @param string $namespace + * @param AuthorizationInterface|null $authorization * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -117,7 +125,8 @@ public function __construct( Processor $processor, UiComponentFactory $uiComponentFactory, DataProviderInterface $dataProvider = null, - $namespace = null + $namespace = null, + AuthorizationInterface $authorization = null ) { $this->namespace = $namespace; $this->request = $request; @@ -129,6 +138,9 @@ public function __construct( $this->urlBuilder = $urlBuilder; $this->processor = $processor; $this->uiComponentFactory = $uiComponentFactory; + $this->authorization = $authorization ?: ObjectManager::getInstance()->get( + AuthorizationInterface::class + ); $this->setAcceptType(); } @@ -280,6 +292,9 @@ public function addButtons(array $buttons, UiComponentInterface $component) uasort($buttons, [$this, 'sortButtons']); foreach ($buttons as $buttonId => $buttonData) { + if (isset($buttonData['aclResource']) && !$this->authorization->isAllowed($buttonData['aclResource'])) { + continue; + } if (isset($buttonData['url'])) { $buttonData['url'] = $this->getUrl($buttonData['url']); } diff --git a/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php b/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php index 266a1f873f4b7..a585eda37df68 100644 --- a/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php +++ b/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php @@ -37,7 +37,7 @@ class GeneratorPool * @param ScheduledStructure\Helper $helper * @param ConditionFactory $conditionFactory * @param \Psr\Log\LoggerInterface $logger - * @param array $generators + * @param array|null $generators */ public function __construct( ScheduledStructure\Helper $helper, @@ -67,7 +67,7 @@ public function getGenerator($type) } /** - * Traverse through all generators and generate all scheduled elements + * Traverse through all generators and generate all scheduled elements. * * @param Reader\Context $readerContext * @param Generator\Context $generatorContext @@ -86,11 +86,17 @@ public function process(Reader\Context $readerContext, Generator\Context $genera * Add generators to pool * * @param GeneratorInterface[] $generators + * * @return void */ protected function addGenerators(array $generators) { foreach ($generators as $generator) { + if (!$generator instanceof GeneratorInterface) { + throw new \InvalidArgumentException( + sprintf('Generator class must be an instance of %s', GeneratorInterface::class) + ); + } $this->generators[$generator->getType()] = $generator; } } @@ -131,9 +137,9 @@ protected function buildStructure(ScheduledStructure $scheduledStructure, Data\S } /** - * Reorder a child of a specified element + * Reorder a child of a specified element. * - * @param ScheduledStructure $scheduledStructure, + * @param ScheduledStructure $scheduledStructure * @param Data\Structure $structure * @param string $elementName * @return void @@ -227,6 +233,8 @@ protected function moveElementInStructure( } /** + * Check visibility conditions exists in data. + * * @param array $data * * @return bool diff --git a/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd b/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd index 39cdec05a65ea..6486b39070788 100755 --- a/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd +++ b/lib/internal/Magento/Framework/View/Layout/etc/elements.xsd @@ -313,6 +313,7 @@ + diff --git a/lib/internal/Magento/Framework/View/Layout/etc/head.xsd b/lib/internal/Magento/Framework/View/Layout/etc/head.xsd index a913507ae17b3..15762dc2f0ae6 100644 --- a/lib/internal/Magento/Framework/View/Layout/etc/head.xsd +++ b/lib/internal/Magento/Framework/View/Layout/etc/head.xsd @@ -20,6 +20,15 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php index 823ad5c8c8be7..c23f13745c79f 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergeStrategy/DirectTest.php @@ -5,13 +5,19 @@ */ namespace Magento\Framework\View\Test\Unit\Asset\MergeStrategy; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use \Magento\Framework\View\Asset\MergeStrategy\Direct; - use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\View\Asset\MergeStrategy\Direct; +/** + * Test for Magento\Framework\View\Asset\MergeStrategy\Direct. + */ class DirectTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Math\Random|\PHPUnit_Framework_MockObject_MockObject + */ + protected $mathRandomMock; /** * @var \Magento\Framework\View\Asset\MergeStrategy\Direct */ @@ -50,33 +56,47 @@ protected function setUp() [DirectoryList::TMP, \Magento\Framework\Filesystem\DriverPool::FILE, $this->tmpDir], ]); $this->resultAsset = $this->createMock(\Magento\Framework\View\Asset\File::class); - $this->object = new Direct($filesystem, $this->cssUrlResolver); + $this->mathRandomMock = $this->getMockBuilder(\Magento\Framework\Math\Random::class) + ->disableOriginalConstructor() + ->getMock(); + $this->object = new Direct($filesystem, $this->cssUrlResolver, $this->mathRandomMock); } public function testMergeNoAssets() { + $uniqId = '_b3bf82fa6e140594420fa90982a8e877'; $this->resultAsset->expects($this->once())->method('getPath')->will($this->returnValue('foo/result')); $this->staticDir->expects($this->never())->method('writeFile'); - $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result', ''); - $this->tmpDir->expects($this->once())->method('renameFile')->with('foo/result', 'foo/result', $this->staticDir); + $this->mathRandomMock->expects($this->once()) + ->method('getUniqueHash') + ->willReturn($uniqId); + $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result' . $uniqId, ''); + $this->tmpDir->expects($this->once())->method('renameFile') + ->with('foo/result' . $uniqId, 'foo/result', $this->staticDir); $this->object->merge([], $this->resultAsset); } public function testMergeGeneric() { + $uniqId = '_be50ccf992fd81818c1a2645d1a29e92'; $this->resultAsset->expects($this->once())->method('getPath')->will($this->returnValue('foo/result')); $assets = $this->prepareAssetsToMerge([' one', 'two']); // note leading space intentionally $this->staticDir->expects($this->never())->method('writeFile'); - $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result', 'onetwo'); - $this->tmpDir->expects($this->once())->method('renameFile')->with('foo/result', 'foo/result', $this->staticDir); + $this->mathRandomMock->expects($this->once()) + ->method('getUniqueHash') + ->willReturn($uniqId); + $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result' . $uniqId, 'onetwo'); + $this->tmpDir->expects($this->once())->method('renameFile') + ->with('foo/result' . $uniqId, 'foo/result', $this->staticDir); $this->object->merge($assets, $this->resultAsset); } public function testMergeCss() { + $uniqId = '_f929c374767e00712449660ea673f2f5'; $this->resultAsset->expects($this->exactly(3)) ->method('getPath') - ->will($this->returnValue('foo/result')); + ->willReturn('foo/result'); $this->resultAsset->expects($this->any())->method('getContentType')->will($this->returnValue('css')); $assets = $this->prepareAssetsToMerge(['one', 'two']); $this->cssUrlResolver->expects($this->exactly(2)) @@ -85,10 +105,14 @@ public function testMergeCss() $this->cssUrlResolver->expects($this->once()) ->method('aggregateImportDirectives') ->with('12') - ->will($this->returnValue('1020')); + ->willReturn('1020'); + $this->mathRandomMock->expects($this->once()) + ->method('getUniqueHash') + ->willReturn($uniqId); $this->staticDir->expects($this->never())->method('writeFile'); - $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result', '1020'); - $this->tmpDir->expects($this->once())->method('renameFile')->with('foo/result', 'foo/result', $this->staticDir); + $this->tmpDir->expects($this->once())->method('writeFile')->with('foo/result' . $uniqId, '1020'); + $this->tmpDir->expects($this->once())->method('renameFile') + ->with('foo/result' . $uniqId, 'foo/result', $this->staticDir); $this->object->merge($assets, $this->resultAsset); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php index a8beb380c5155..5654563f87981 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/RepositoryTest.php @@ -164,6 +164,50 @@ public function testUpdateDesignParams($params, $result) $this->assertEquals($result, $params); } + /** + * @return void + */ + public function testUpdateDesignParamsWithThemePath() + { + $params = ['area' => 'AREA']; + $result = ['area' => 'AREA', 'themeModel' => 'Theme', 'module' => false, 'locale' => null]; + + $this->designMock + ->expects($this->once()) + ->method('getConfigurationDesignTheme') + ->willReturn('themePath'); + + $this->themeProvider + ->expects($this->once()) + ->method('getThemeByFullPath') + ->willReturn('Theme'); + + $this->repository->updateDesignParams($params); + $this->assertEquals($result, $params); + } + + /** + * @return void + */ + public function testUpdateDesignParamsWithThemeId() + { + $params = ['area' => 'AREA']; + $result = ['area' => 'AREA', 'themeModel' => 'Theme', 'module' => false, 'locale' => null]; + + $this->designMock + ->expects($this->once()) + ->method('getConfigurationDesignTheme') + ->willReturn('1'); + + $this->themeProvider + ->expects($this->once()) + ->method('getThemeById') + ->willReturn('Theme'); + + $this->repository->updateDesignParams($params); + $this->assertEquals($result, $params); + } + /** * @return array */ diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php index 5f7508438a6ed..dba775ea894f4 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php @@ -6,6 +6,7 @@ namespace Magento\Framework\View\Test\Unit\Element; +use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\Context; use Magento\Framework\Config\View; @@ -13,7 +14,6 @@ use Magento\Framework\Event\ManagerInterface as EventManagerInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Cache\StateInterface as CacheStateInterface; -use Magento\Framework\App\CacheInterface; use Magento\Framework\Session\SidResolverInterface; use Magento\Framework\Session\SessionManagerInterface; @@ -42,11 +42,6 @@ class AbstractBlockTest extends \PHPUnit\Framework\TestCase */ private $cacheStateMock; - /** - * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $cacheMock; - /** * @var SidResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -57,6 +52,11 @@ class AbstractBlockTest extends \PHPUnit\Framework\TestCase */ private $sessionMock; + /** + * @var LockGuardedCacheLoader|\PHPUnit_Framework_MockObject_MockObject + */ + private $lockQuery; + /** * @return void */ @@ -65,7 +65,10 @@ protected function setUp() $this->eventManagerMock = $this->getMockForAbstractClass(EventManagerInterface::class); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->cacheStateMock = $this->getMockForAbstractClass(CacheStateInterface::class); - $this->cacheMock = $this->getMockForAbstractClass(CacheInterface::class); + $this->lockQuery = $this->getMockBuilder(LockGuardedCacheLoader::class) + ->disableOriginalConstructor() + ->setMethods(['lockedLoadData']) + ->getMockForAbstractClass(); $this->sidResolverMock = $this->getMockForAbstractClass(SidResolverInterface::class); $this->sessionMock = $this->getMockForAbstractClass(SessionManagerInterface::class); $contextMock = $this->createMock(Context::class); @@ -78,9 +81,6 @@ protected function setUp() $contextMock->expects($this->once()) ->method('getCacheState') ->willReturn($this->cacheStateMock); - $contextMock->expects($this->once()) - ->method('getCache') - ->willReturn($this->cacheMock); $contextMock->expects($this->once()) ->method('getSidResolver') ->willReturn($this->sidResolverMock); @@ -89,7 +89,11 @@ protected function setUp() ->willReturn($this->sessionMock); $this->block = $this->getMockForAbstractClass( AbstractBlock::class, - ['context' => $contextMock] + [ + 'context' => $contextMock, + 'data' => [], + 'lockQuery' => $this->lockQuery + ] ); } @@ -219,10 +223,7 @@ public function testToHtmlWhenModuleIsDisabled() /** * @param string|bool $cacheLifetime * @param string|bool $dataFromCache - * @param string $dataForSaveCache * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectsDispatchEvent - * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectsCacheLoad - * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount $expectsCacheSave * @param string $expectedResult * @return void * @dataProvider getCacheLifetimeDataProvider @@ -230,10 +231,7 @@ public function testToHtmlWhenModuleIsDisabled() public function testGetCacheLifetimeViaToHtml( $cacheLifetime, $dataFromCache, - $dataForSaveCache, $expectsDispatchEvent, - $expectsCacheLoad, - $expectsCacheSave, $expectedResult ) { $moduleName = 'Test'; @@ -252,13 +250,9 @@ public function testGetCacheLifetimeViaToHtml( ->method('isEnabled') ->with(AbstractBlock::CACHE_GROUP) ->willReturn(true); - $this->cacheMock->expects($expectsCacheLoad) - ->method('load') - ->with(AbstractBlock::CACHE_KEY_PREFIX . $cacheKey) + $this->lockQuery->expects($this->any()) + ->method('lockedLoadData') ->willReturn($dataFromCache); - $this->cacheMock->expects($expectsCacheSave) - ->method('save') - ->with($dataForSaveCache, AbstractBlock::CACHE_KEY_PREFIX . $cacheKey); $this->sidResolverMock->expects($this->any()) ->method('getSessionIdQueryParam') ->with($this->sessionMock) @@ -279,46 +273,31 @@ public function getCacheLifetimeDataProvider() [ 'cacheLifetime' => null, 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->never(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => '', ], [ 'cacheLifetime' => false, 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->never(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => '', ], [ 'cacheLifetime' => 120, 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->once(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => 'dataFromCache', ], [ 'cacheLifetime' => '120string', 'dataFromCache' => 'dataFromCache', - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->once(), - 'expectsCacheSave' => $this->never(), 'expectedResult' => 'dataFromCache', ], [ 'cacheLifetime' => 120, 'dataFromCache' => false, - 'dataForSaveCache' => '', 'expectsDispatchEvent' => $this->exactly(2), - 'expectsCacheLoad' => $this->once(), - 'expectsCacheSave' => $this->once(), 'expectedResult' => '', ], ]; diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php index b911a38dbb488..4c76087bfea12 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\View\Test\Unit\Element\Html; class LinkTest extends \PHPUnit\Framework\TestCase { + private $objectManager; + + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + } + /** * @var array */ @@ -24,24 +32,8 @@ class LinkTest extends \PHPUnit\Framework\TestCase */ protected $link; - /** - * @param \Magento\Framework\View\Element\Html\Link $link - * @param string $expected - * - * @dataProvider getLinkAttributesDataProvider - */ - public function testGetLinkAttributes($link, $expected) - { - $this->assertEquals($expected, $link->getLinkAttributes()); - } - - /** - * @return array - */ - public function getLinkAttributesDataProvider() + public function testGetLinkAttributes() { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->setMethods(['escapeHtml'])->disableOriginalConstructor()->getMock(); @@ -54,13 +46,19 @@ public function getLinkAttributesDataProvider() $urlBuilderMock->expects($this->any()) ->method('getUrl') - ->will($this->returnArgument('http://site.com/link.html')); + ->willReturn('http://site.com/link.html'); $validtorMock = $this->getMockBuilder(\Magento\Framework\View\Element\Template\File\Validator::class) ->setMethods(['isValid'])->disableOriginalConstructor()->getMock(); + $validtorMock->expects($this->any()) + ->method('isValid') + ->willReturn(false); $scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config::class) ->setMethods(['isSetFlag'])->disableOriginalConstructor()->getMock(); + $scopeConfigMock->expects($this->any()) + ->method('isSetFlag') + ->willReturn(true); $resolverMock = $this->getMockBuilder(\Magento\Framework\View\Element\Template\File\Resolver::class) ->setMethods([])->disableOriginalConstructor()->getMock(); @@ -72,48 +70,48 @@ public function getLinkAttributesDataProvider() $contextMock->expects($this->any()) ->method('getValidator') - ->will($this->returnValue($validtorMock)); + ->willReturn($validtorMock); $contextMock->expects($this->any()) ->method('getResolver') - ->will($this->returnValue($resolverMock)); + ->willReturn($resolverMock); $contextMock->expects($this->any()) ->method('getEscaper') - ->will($this->returnValue($escaperMock)); + ->willReturn($escaperMock); $contextMock->expects($this->any()) ->method('getUrlBuilder') - ->will($this->returnValue($urlBuilderMock)); + ->willReturn($urlBuilderMock); $contextMock->expects($this->any()) ->method('getScopeConfig') - ->will($this->returnValue($scopeConfigMock)); + ->willReturn($scopeConfigMock); /** @var \Magento\Framework\View\Element\Html\Link $linkWithAttributes */ - $linkWithAttributes = $objectManagerHelper->getObject( + $linkWithAttributes = $this->objectManager->getObject( \Magento\Framework\View\Element\Html\Link::class, ['context' => $contextMock] ); + + $this->assertEquals( + 'href="http://site.com/link.html"', + $linkWithAttributes->getLinkAttributes() + ); + /** @var \Magento\Framework\View\Element\Html\Link $linkWithoutAttributes */ - $linkWithoutAttributes = $objectManagerHelper->getObject( + $linkWithoutAttributes = $this->objectManager->getObject( \Magento\Framework\View\Element\Html\Link::class, ['context' => $contextMock] ); - foreach ($this->allowedAttributes as $attribute) { - $linkWithAttributes->setDataUsingMethod($attribute, $attribute); + $linkWithoutAttributes->setDataUsingMethod($attribute, $attribute); } - return [ - 'full' => [ - 'link' => $linkWithAttributes, - 'expected' => 'shape="shape" tabindex="tabindex" onfocus="onfocus" onblur="onblur" id="id"', - ], - 'empty' => [ - 'link' => $linkWithoutAttributes, - 'expected' => '', - ], - ]; + $this->assertEquals( + 'href="http://site.com/link.html" shape="shape" tabindex="tabindex"' + . ' onfocus="onfocus" onblur="onblur" id="id"', + $linkWithoutAttributes->getLinkAttributes() + ); } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php index b7301c4cad5d4..75c7fc248541c 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php @@ -11,7 +11,11 @@ use Magento\Framework\View\Element\UiComponent\Context; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\UiComponent\Control\ActionPoolInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ContextTest extends \PHPUnit\Framework\TestCase { /** @@ -19,6 +23,16 @@ class ContextTest extends \PHPUnit\Framework\TestCase */ protected $context; + /** + * @var ActionPoolInterface + */ + private $actionPool; + + /** + * @var \Magento\Framework\AuthorizationInterface + */ + private $authorization; + protected function setUp() { $pageLayout = $this->getMockBuilder(\Magento\Framework\View\LayoutInterface::class)->getMock(); @@ -33,6 +47,10 @@ protected function setUp() $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Control\ActionPoolFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->actionPool = $this->getMockBuilder(ActionPoolInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $actionPoolFactory->method('create')->willReturn($this->actionPool); $contentTypeFactory = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContentType\ContentTypeFactory::class) ->disableOriginalConstructor() @@ -43,6 +61,9 @@ protected function setUp() $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->authorization = $this->getMockBuilder(\Magento\Framework\AuthorizationInterface::class) + ->disableOriginalConstructor() + ->getMock(); $objectManagerHelper = new ObjectManagerHelper($this); $this->context = $objectManagerHelper->getObject( @@ -55,11 +76,62 @@ protected function setUp() 'contentTypeFactory' => $contentTypeFactory, 'urlBuilder' => $urlBuilder, 'processor' => $processor, - 'uiComponentFactory' => $uiComponentFactory + 'uiComponentFactory' => $uiComponentFactory, + 'authorization' => $this->authorization, ] ); } + public function testAddButtonWithoutAclResource() + { + $component = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->actionPool->expects($this->once())->method('add'); + $this->authorization->expects($this->never())->method('isAllowed'); + + $this->context->addButtons([ + 'button_1' => [ + 'name' => 'button_1', + ], + ], $component); + } + + public function testAddButtonWithAclResourceAllowed() + { + $component = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->actionPool->expects($this->once())->method('add'); + $this->authorization->expects($this->once())->method('isAllowed')->willReturn(true); + + $this->context->addButtons([ + 'button_1' => [ + 'name' => 'button_1', + 'aclResource' => 'Magento_Framwork::acl', + ], + ], $component); + } + + public function testAddButtonWithAclResourceDenied() + { + $component = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->actionPool->expects($this->never())->method('add'); + $this->authorization->expects($this->once())->method('isAllowed')->willReturn(false); + + $this->context->addButtons([ + 'button_1' => [ + 'name' => 'button_1', + 'aclResource' => 'Magento_Framwork::acl', + ], + ], $component); + } + /** * @dataProvider addComponentDefinitionDataProvider * @param array $components diff --git a/lib/internal/Magento/Framework/Webapi/CustomAttribute/PreprocessorInterface.php b/lib/internal/Magento/Framework/Webapi/CustomAttribute/PreprocessorInterface.php new file mode 100644 index 0000000000000..8de30e92218c0 --- /dev/null +++ b/lib/internal/Magento/Framework/Webapi/CustomAttribute/PreprocessorInterface.php @@ -0,0 +1,36 @@ +typeProcessor = $typeProcessor; $this->objectManager = $objectManager; @@ -101,6 +114,7 @@ public function __construct( ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ServiceTypeToEntityTypeMap::class); $this->config = $config ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ConfigInterface::class); + $this->customAttributePreprocessors = $customAttributePreprocessors; } /** @@ -132,7 +146,6 @@ private function getNameFinder() * @param string $serviceMethodName name of the method that we are trying to call * @param array $inputArray data to send to method in key-value format * @return array list of parameters that can be used to call the service method - * @throws InputException if no value is provided for required parameters * @throws WebapiException */ public function process($serviceClassName, $serviceMethodName, array $inputArray) @@ -165,17 +178,25 @@ public function process($serviceClassName, $serviceMethodName, array $inputArray } /** + * Retrieve constructor data + * * @param string $className * @param array $data * @return array * @throws \ReflectionException + * @throws \Magento\Framework\Exception\LocalizedException */ private function getConstructorData(string $className, array $data): array { $preferenceClass = $this->config->getPreference($className); $class = new ClassReflection($preferenceClass ?: $className); - $constructor = $class->getConstructor(); + try { + $constructor = $class->getMethod('__construct'); + } catch (\ReflectionException $e) { + $constructor = null; + } + if ($constructor === null) { return []; } @@ -184,7 +205,15 @@ private function getConstructorData(string $className, array $data): array $parameters = $constructor->getParameters(); foreach ($parameters as $parameter) { if (isset($data[$parameter->getName()])) { - $res[$parameter->getName()] = $data[$parameter->getName()]; + $parameterType = $this->typeProcessor->getParamType($parameter); + + try { + $res[$parameter->getName()] = $this->convertValue($data[$parameter->getName()], $parameterType); + } catch (\ReflectionException $e) { + // Parameter was not correclty declared or the class is uknown. + // By not returing the contructor value, we will automatically fall back to the "setters" way. + continue; + } } } @@ -200,6 +229,7 @@ private function getConstructorData(string $className, array $data): array * @param array $data * @return object the newly created and populated object * @throws \Exception + * @throws SerializationException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _createFromArray($className, $data) @@ -245,6 +275,7 @@ protected function _createFromArray($className, $data) } else { $setterValue = $this->convertValue($value, $returnType); } + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (SerializationException $e) { throw new SerializationException( new Phrase( @@ -273,12 +304,11 @@ protected function convertCustomAttributeValue($customAttributesValueArray, $dat $dataObjectClassName = ltrim($dataObjectClassName, '\\'); foreach ($customAttributesValueArray as $key => $customAttribute) { + $this->runCustomAttributePreprocessors($key, $customAttribute); if (!is_array($customAttribute)) { $customAttribute = [AttributeValue::ATTRIBUTE_CODE => $key, AttributeValue::VALUE => $customAttribute]; } - list($customAttributeCode, $customAttributeValue) = $this->processCustomAttribute($customAttribute); - $entityType = $this->serviceTypeToEntityTypeMap->getEntityType($dataObjectClassName); if ($entityType) { $type = $this->customAttributeTypeLocator->getType( @@ -294,6 +324,7 @@ protected function convertCustomAttributeValue($customAttributesValueArray, $dat ) { try { $attributeValue = $this->convertValue($customAttributeValue, $type); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (SerializationException $e) { throw new SerializationException( new Phrase( @@ -315,11 +346,49 @@ protected function convertCustomAttributeValue($customAttributesValueArray, $dat return $result; } + /** + * Get map of preprocessors related to the custom attributes + * + * @return array + */ + private function getAttributesPreprocessorsMap(): array + { + if (!$this->attributesPreprocessorsMap) { + foreach ($this->customAttributePreprocessors as $attributePreprocessor) { + foreach ($attributePreprocessor->getAffectedAttributes() as $attributeKey) { + $this->attributesPreprocessorsMap[$attributeKey][] = $attributePreprocessor; + } + } + } + + return $this->attributesPreprocessorsMap; + } + + /** + * Prepare attribute value by loaded attribute preprocessors + * + * @param mixed $key + * @param mixed $customAttribute + */ + private function runCustomAttributePreprocessors($key, &$customAttribute) + { + $preprocessorsMap = $this->getAttributesPreprocessorsMap(); + if ($key && is_array($customAttribute) && array_key_exists($key, $preprocessorsMap)) { + $preprocessorsList = $preprocessorsMap[$key]; + foreach ($preprocessorsList as $attributePreprocessor) { + if ($attributePreprocessor->shouldBeProcessed($key, $customAttribute)) { + $attributePreprocessor->process($key, $customAttribute); + } + } + } + } + /** * Derive the custom attribute code and value. * * @param string[] $customAttribute * @return string[] + * @throws SerializationException */ private function processCustomAttribute($customAttribute) { diff --git a/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php b/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php index cdb6ed799aade..224421d6561c8 100644 --- a/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php @@ -7,8 +7,11 @@ use Magento\Framework\Api\AbstractExtensibleObject; use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Reflection\MethodsMap; +use Magento\Framework\Reflection\TypeProcessor; +use Zend\Code\Reflection\ClassReflection; /** * Data object converter @@ -27,16 +30,24 @@ class ServiceOutputProcessor implements ServicePayloadConverterInterface */ protected $methodsMapProcessor; + /** + * @var TypeProcessor|null + */ + private $typeProcessor; + /** * @param DataObjectProcessor $dataObjectProcessor * @param MethodsMap $methodsMapProcessor + * @param TypeProcessor|null $typeProcessor */ public function __construct( DataObjectProcessor $dataObjectProcessor, - MethodsMap $methodsMapProcessor + MethodsMap $methodsMapProcessor, + TypeProcessor $typeProcessor = null ) { $this->dataObjectProcessor = $dataObjectProcessor; $this->methodsMapProcessor = $methodsMapProcessor; + $this->typeProcessor = $typeProcessor ?: ObjectManager::getInstance()->get(TypeProcessor::class); } /** @@ -57,6 +68,12 @@ public function process($data, $serviceClassName, $serviceMethodName) { /** @var string $dataType */ $dataType = $this->methodsMapProcessor->getMethodReturnType($serviceClassName, $serviceMethodName); + + if (class_exists($serviceClassName) || interface_exists($serviceClassName)) { + $sourceClass = new ClassReflection($serviceClassName); + $dataType = $this->typeProcessor->resolveFullyQualifiedClassName($sourceClass, $dataType); + } + return $this->convertValue($data, $dataType); } diff --git a/lib/web/css/source/components/_modals.less b/lib/web/css/source/components/_modals.less index a8e8cebde3a6c..396930cce6d86 100644 --- a/lib/web/css/source/components/_modals.less +++ b/lib/web/css/source/components/_modals.less @@ -102,7 +102,7 @@ &.confirm { .modal-inner-wrap { - .lib-css(width, @modal-popup-confirm__width); + .lib-css(max-width, @modal-popup-confirm__width); .modal-content { padding-right: 7rem; diff --git a/lib/web/css/source/lib/_icons.less b/lib/web/css/source/lib/_icons.less index d113935e2b1cd..abb8b43368f13 100644 --- a/lib/web/css/source/lib/_icons.less +++ b/lib/web/css/source/lib/_icons.less @@ -25,9 +25,12 @@ @_icon-font-text-hide: @icon-font__text-hide, @_icon-font-display: @icon-font__display ) when (@_icon-font-position = before) { - ._lib-icon-text-hide(@_icon-font-text-hide); .lib-css(display, @_icon-font-display); - text-decoration: none; + text-decoration: none; + + & when not (@_icon-font-content = false) { + ._lib-icon-text-hide(@_icon-font-text-hide); + } &:before { ._lib-icon-font( @@ -68,10 +71,13 @@ @_icon-font-text-hide: @icon-font__text-hide, @_icon-font-display: @icon-font__display ) when (@_icon-font-position = after) { - ._lib-icon-text-hide(@_icon-font-text-hide); .lib-css(display, @_icon-font-display); text-decoration: none; - + + & when not (@_icon-font-content = false) { + ._lib-icon-text-hide(@_icon-font-text-hide); + } + &:after { ._lib-icon-font( @_icon-font-content, @@ -151,8 +157,11 @@ @_icon-image-text-hide: @icon__text-hide ) when (@_icon-image-position = before) { display: inline-block; - ._lib-icon-text-hide(@_icon-image-text-hide); - + + & when not (@_icon-image = false) { + ._lib-icon-text-hide(@_icon-image-text-hide); + } + &:before { ._lib-icon-image( @_icon-image, @@ -179,7 +188,10 @@ @_icon-image-text-hide: @icon__text-hide ) when (@_icon-image-position = after) { display: inline-block; - ._lib-icon-text-hide(@_icon-image-text-hide); + + & when not (@_icon-image = false) { + ._lib-icon-text-hide(@_icon-font-text-hide); + } &:after { ._lib-icon-image( diff --git a/lib/web/css/source/lib/_resets.less b/lib/web/css/source/lib/_resets.less index 8228a07ef92ab..08d16842b849c 100644 --- a/lib/web/css/source/lib/_resets.less +++ b/lib/web/css/source/lib/_resets.less @@ -4,7 +4,7 @@ // */ // -// Resetes +// Resets // _____________________________________________ // diff --git a/lib/web/css/source/lib/variables/_colors.less b/lib/web/css/source/lib/variables/_colors.less index 9c694468e9f62..ffb0e8e797d81 100644 --- a/lib/web/css/source/lib/variables/_colors.less +++ b/lib/web/css/source/lib/variables/_colors.less @@ -7,9 +7,13 @@ // Color variables // _____________________________________________ +@color-blue-dodger: #008bdb; +@color-black_dark: #333333; + @color-white: #fff; @color-black: #000; +@color-darkie-gray: #8a837f; @color-gray19: #303030; @color-gray20: #333; @color-gray34: #575757; @@ -29,12 +33,16 @@ @color-gray79: #c9c9c9; @color-gray80: #ccc; @color-gray82: #d1d1d1; +@color-gray83: #d4d4d4; @color-gray89: #e3e3e3; @color-gray90: #e5e5e5; @color-gray91: #e8e8e8; @color-gray92: #ebebeb; @color-gray94: #f0f0f0; @color-gray95: #f2f2f2; +@color-gray_light: #cccccc; +@color-lighter-grayish: #cacaca; +@color-very-dark-gray: #666; @color-white-smoke: #f5f5f5; @color-white-dark-smoke: #efefef; @color-white-fog: #f8f8f8; @@ -80,6 +88,8 @@ @color-pink1: #fae5e5; @color-dark-pink1: #800080; // Legacy pink +@color-brownie: #514943; +@color-brownie-vanilla: #736963; @color-brownie1: #6f4400; @color-brownie-light1: #c07600; diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 093ee707d3f98..b38e70d915c9d 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -1399,12 +1399,13 @@ fotoramaVersion = '4.6.4'; touchFLAG, targetIsSelectFLAG, targetIsLinkFlag, + isDisabledSwipe, tolerance, moved; function onStart(e) { $target = $(e.target); - tail.checked = targetIsSelectFLAG = targetIsLinkFlag = moved = false; + tail.checked = targetIsSelectFLAG = targetIsLinkFlag = isDisabledSwipe = moved = false; if (touchEnabledFLAG || tail.flow @@ -1415,6 +1416,7 @@ fotoramaVersion = '4.6.4'; touchFLAG = e.type === 'touchstart'; targetIsLinkFlag = $target.is('a, a *', el); + isDisabledSwipe = $target.hasClass('disableSwipe'); controlTouch = tail.control; tolerance = (tail.noMove || tail.noSwipe || controlTouch) ? 16 : !tail.snap ? 4 : 0; @@ -1428,7 +1430,7 @@ fotoramaVersion = '4.6.4'; touchEnabledFLAG = tail.flow = true; - if (!touchFLAG || tail.go) stopEvent(e); + if (!isDisabledSwipe && (!touchFLAG || tail.go)) stopEvent(e); } function onMove(e) { @@ -1441,6 +1443,12 @@ fotoramaVersion = '4.6.4'; return; } + isDisabledSwipe = $(e.target).hasClass('disableSwipe'); + + if (isDisabledSwipe) { + return; + } + extendEvent(e); var xDiff = Math.abs(e._x - startEvent._x), // opt _x → _pageX diff --git a/lib/web/fotorama/fotorama.min.js b/lib/web/fotorama/fotorama.min.js deleted file mode 100644 index 0a0cbd9db7e74..0000000000000 --- a/lib/web/fotorama/fotorama.min.js +++ /dev/null @@ -1 +0,0 @@ -fotoramaVersion="4.6.4";(function(window,document,location,$,undefined){"use strict";var _fotoramaClass="fotorama",_fullscreenClass="fotorama__fullscreen",wrapClass=_fotoramaClass+"__wrap",wrapCss2Class=wrapClass+"--css2",wrapCss3Class=wrapClass+"--css3",wrapVideoClass=wrapClass+"--video",wrapFadeClass=wrapClass+"--fade",wrapSlideClass=wrapClass+"--slide",wrapNoControlsClass=wrapClass+"--no-controls",wrapNoShadowsClass=wrapClass+"--no-shadows",wrapPanYClass=wrapClass+"--pan-y",wrapRtlClass=wrapClass+"--rtl",wrapOnlyActiveClass=wrapClass+"--only-active",wrapNoCaptionsClass=wrapClass+"--no-captions",wrapToggleArrowsClass=wrapClass+"--toggle-arrows",stageClass=_fotoramaClass+"__stage",stageFrameClass=stageClass+"__frame",stageFrameVideoClass=stageFrameClass+"--video",stageShaftClass=stageClass+"__shaft",grabClass=_fotoramaClass+"__grab",pointerClass=_fotoramaClass+"__pointer",arrClass=_fotoramaClass+"__arr",arrDisabledClass=arrClass+"--disabled",arrPrevClass=arrClass+"--prev",arrNextClass=arrClass+"--next",navClass=_fotoramaClass+"__nav",navWrapClass=navClass+"-wrap",navShaftClass=navClass+"__shaft",navShaftVerticalClass=navWrapClass+"--vertical",navShaftListClass=navWrapClass+"--list",navShafthorizontalClass=navWrapClass+"--horizontal",navDotsClass=navClass+"--dots",navThumbsClass=navClass+"--thumbs",navFrameClass=navClass+"__frame",fadeClass=_fotoramaClass+"__fade",fadeFrontClass=fadeClass+"-front",fadeRearClass=fadeClass+"-rear",shadowClass=_fotoramaClass+"__shadow",shadowsClass=shadowClass+"s",shadowsLeftClass=shadowsClass+"--left",shadowsRightClass=shadowsClass+"--right",shadowsTopClass=shadowsClass+"--top",shadowsBottomClass=shadowsClass+"--bottom",activeClass=_fotoramaClass+"__active",selectClass=_fotoramaClass+"__select",hiddenClass=_fotoramaClass+"--hidden",fullscreenClass=_fotoramaClass+"--fullscreen",fullscreenIconClass=_fotoramaClass+"__fullscreen-icon",errorClass=_fotoramaClass+"__error",loadingClass=_fotoramaClass+"__loading",loadedClass=_fotoramaClass+"__loaded",loadedFullClass=loadedClass+"--full",loadedImgClass=loadedClass+"--img",grabbingClass=_fotoramaClass+"__grabbing",imgClass=_fotoramaClass+"__img",imgFullClass=imgClass+"--full",thumbClass=_fotoramaClass+"__thumb",thumbArrLeft=thumbClass+"__arr--left",thumbArrRight=thumbClass+"__arr--right",thumbBorderClass=thumbClass+"-border",htmlClass=_fotoramaClass+"__html",videoContainerClass=_fotoramaClass+"-video-container",videoClass=_fotoramaClass+"__video",videoPlayClass=videoClass+"-play",videoCloseClass=videoClass+"-close",horizontalImageClass=_fotoramaClass+"_horizontal_ratio",verticalImageClass=_fotoramaClass+"_vertical_ratio",fotoramaSpinnerClass=_fotoramaClass+"__spinner",spinnerShowClass=fotoramaSpinnerClass+"--show";var JQUERY_VERSION=$&&$.fn.jquery.split(".");if(!JQUERY_VERSION||JQUERY_VERSION[0]<1||JQUERY_VERSION[0]==1&&JQUERY_VERSION[1]<8){throw"Fotorama requires jQuery 1.8 or later and will not run without it."}var _={};var Modernizr=function(window,document,undefined){var version="2.8.3",Modernizr={},docElement=document.documentElement,mod="modernizr",modElem=document.createElement(mod),mStyle=modElem.style,inputElem,toString={}.toString,prefixes=" -webkit- -moz- -o- -ms- ".split(" "),omPrefixes="Webkit Moz O ms",cssomPrefixes=omPrefixes.split(" "),domPrefixes=omPrefixes.toLowerCase().split(" "),tests={},inputs={},attrs={},classes=[],slice=classes.slice,featureName,injectElementWithStyles=function(rule,callback,nodes,testnames){var style,ret,node,docOverflow,div=document.createElement("div"),body=document.body,fakeBody=body||document.createElement("body");if(parseInt(nodes,10)){while(nodes--){node=document.createElement("div");node.id=testnames?testnames[nodes]:mod+(nodes+1);div.appendChild(node)}}style=["­",'"].join("");div.id=mod;(body?div:fakeBody).innerHTML+=style;fakeBody.appendChild(div);if(!body){fakeBody.style.background="";fakeBody.style.overflow="hidden";docOverflow=docElement.style.overflow;docElement.style.overflow="hidden";docElement.appendChild(fakeBody)}ret=callback(div,rule);if(!body){fakeBody.parentNode.removeChild(fakeBody);docElement.style.overflow=docOverflow}else{div.parentNode.removeChild(div)}return!!ret},_hasOwnProperty={}.hasOwnProperty,hasOwnProp;if(!is(_hasOwnProperty,"undefined")&&!is(_hasOwnProperty.call,"undefined")){hasOwnProp=function(object,property){return _hasOwnProperty.call(object,property)}}else{hasOwnProp=function(object,property){return property in object&&is(object.constructor.prototype[property],"undefined")}}if(!Function.prototype.bind){Function.prototype.bind=function bind(that){var target=this;if(typeof target!="function"){throw new TypeError}var args=slice.call(arguments,1),bound=function(){if(this instanceof bound){var F=function(){};F.prototype=target.prototype;var self=new F;var result=target.apply(self,args.concat(slice.call(arguments)));if(Object(result)===result){return result}return self}else{return target.apply(that,args.concat(slice.call(arguments)))}};return bound}}function setCss(str){mStyle.cssText=str}function setCssAll(str1,str2){return setCss(prefixes.join(str1+";")+(str2||""))}function is(obj,type){return typeof obj===type}function contains(str,substr){return!!~(""+str).indexOf(substr)}function testProps(props,prefixed){for(var i in props){var prop=props[i];if(!contains(prop,"-")&&mStyle[prop]!==undefined){return prefixed=="pfx"?prop:true}}return false}function testDOMProps(props,obj,elem){for(var i in props){var item=obj[props[i]];if(item!==undefined){if(elem===false)return props[i];if(is(item,"function")){return item.bind(elem||obj)}return item}}return false}function testPropsAll(prop,prefixed,elem){var ucProp=prop.charAt(0).toUpperCase()+prop.slice(1),props=(prop+" "+cssomPrefixes.join(ucProp+" ")+ucProp).split(" ");if(is(prefixed,"string")||is(prefixed,"undefined")){return testProps(props,prefixed)}else{props=(prop+" "+domPrefixes.join(ucProp+" ")+ucProp).split(" ");return testDOMProps(props,prefixed,elem)}}tests["touch"]=function(){var bool;if("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch){bool=true}else{injectElementWithStyles(["@media (",prefixes.join("touch-enabled),("),mod,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(node){bool=node.offsetTop===9})}return bool};tests["csstransforms3d"]=function(){var ret=!!testPropsAll("perspective");if(ret&&"webkitPerspective"in docElement.style){injectElementWithStyles("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(node,rule){ret=node.offsetLeft===9&&node.offsetHeight===3})}return ret};tests["csstransitions"]=function(){return testPropsAll("transition")};for(var feature in tests){if(hasOwnProp(tests,feature)){featureName=feature.toLowerCase();Modernizr[featureName]=tests[feature]();classes.push((Modernizr[featureName]?"":"no-")+featureName)}}Modernizr.addTest=function(feature,test){if(typeof feature=="object"){for(var key in feature){if(hasOwnProp(feature,key)){Modernizr.addTest(key,feature[key])}}}else{feature=feature.toLowerCase();if(Modernizr[feature]!==undefined){return Modernizr}test=typeof test=="function"?test():test;if(typeof enableClasses!=="undefined"&&enableClasses){docElement.className+=" "+(test?"":"no-")+feature}Modernizr[feature]=test}return Modernizr};setCss("");modElem=inputElem=null;Modernizr._version=version;Modernizr._prefixes=prefixes;Modernizr._domPrefixes=domPrefixes;Modernizr._cssomPrefixes=cssomPrefixes;Modernizr.testProp=function(prop){return testProps([prop])};Modernizr.testAllProps=testPropsAll;Modernizr.testStyles=injectElementWithStyles;Modernizr.prefixed=function(prop,obj,elem){if(!obj){return testPropsAll(prop,"pfx")}else{return testPropsAll(prop,obj,elem)}};return Modernizr}(window,document);var fullScreenApi={ok:false,is:function(){return false},request:function(){},cancel:function(){},event:"",prefix:""},browserPrefixes="webkit moz o ms khtml".split(" ");if(typeof document.cancelFullScreen!="undefined"){fullScreenApi.ok=true}else{for(var i=0,il=browserPrefixes.length;i=max?"bottom":"top bottom":pos<=min?"left":pos>=max?"right":"left right"}function smartClick($el,fn,_options){_options=_options||{};$el.each(function(){var $this=$(this),thisData=$this.data(),startEvent;if(thisData.clickOn)return;thisData.clickOn=true;$.extend(touch($this,{onStart:function(e){startEvent=e;(_options.onStart||noop).call(this,e)},onMove:_options.onMove||noop,onTouchEnd:_options.onTouchEnd||noop,onEnd:function(result){if(result.moved)return;fn.call(this,startEvent)}}),{noMove:true})})}function div(classes,child){return'
'+(child||"")+"
"}function cls(className){return"."+className}function createVideoFrame(videoItem){var frame='';return frame}function shuffle(array){var l=array.length;while(l){var i=Math.floor(Math.random()*l--);var t=array[l];array[l]=array[i];array[i]=t}return array}function clone(array){return Object.prototype.toString.call(array)=="[object Array]"&&$.map(array,function(frame){return $.extend({},frame)})}function lockScroll($el,left,top){$el.scrollLeft(left||0).scrollTop(top||0)}function optionsToLowerCase(options){if(options){var opts={};$.each(options,function(key,value){opts[key.toLowerCase()]=value});return opts}}function getRatio(_ratio){if(!_ratio)return;var ratio=+_ratio;if(!isNaN(ratio)){return ratio}else{ratio=_ratio.split("/");return+ratio[0]/+ratio[1]||undefined}}function addEvent(el,e,fn,bool){if(!e)return;el.addEventListener?el.addEventListener(e,fn,!!bool):el.attachEvent("on"+e,fn)}function validateRestrictions(position,restriction){if(position>restriction.max){position=restriction.max}else{if(position=wrapSize-offsetNav){if(dir==="horizontal"){position=-$guessNavFrame.position().left}else{position=-$guessNavFrame.position().top}}else{if((size+opt.margin)*guessIndex<=Math.abs(offsetNav)){if(dir==="horizontal"){position=-$guessNavFrame.position().left+wrapSize-(size+opt.margin)}else{position=-$guessNavFrame.position().top+wrapSize-(size+opt.margin)}}else{position=offsetNav}}position=validateRestrictions(position,navShaftTouchTail);return position||0}function elIsDisabled(el){return!!el.getAttribute("disabled")}function disableAttr(FLAG,disable){if(disable){return{disabled:FLAG}}else{return{tabindex:FLAG*-1+"",disabled:FLAG}}}function addEnterUp(el,fn){addEvent(el,"keyup",function(e){elIsDisabled(el)||e.keyCode==13&&fn.call(el,e)})}function addFocus(el,fn){addEvent(el,"focus",el.onfocusin=function(e){fn.call(el,e)},true)}function stopEvent(e,stopPropagation){e.preventDefault?e.preventDefault():e.returnValue=false;stopPropagation&&e.stopPropagation&&e.stopPropagation()}function stubEvent($el,eventType){var isIOS=/ip(ad|hone|od)/i.test(window.navigator.userAgent);if(isIOS&&eventType==="touchend"){$el.on("touchend",function(e){$DOCUMENT.trigger("mouseup",e)})}$el.on(eventType,function(e){stopEvent(e,true);return false})}function getDirectionSign(forward){return forward?">":"<"}var UTIL=function(){function setRatioClass($el,wh,ht){var rateImg=wh/ht;if(rateImg<=1){$el.parent().removeClass(horizontalImageClass);$el.parent().addClass(verticalImageClass)}else{$el.parent().removeClass(verticalImageClass);$el.parent().addClass(horizontalImageClass)}}function setThumbAttr($frame,value,searchAttr){var attr=searchAttr;if(!$frame.attr(attr)&&$frame.attr(attr)!==undefined){$frame.attr(attr,value)}if($frame.find("["+attr+"]").length){$frame.find("["+attr+"]").each(function(){$(this).attr(attr,value)})}}function isExpectedCaption(frameItem,isExpected,undefined){var expected=false,frameExpected;frameItem.showCaption===undefined||frameItem.showCaption===true?frameExpected=true:frameExpected=false;if(!isExpected){return false}if(frameItem.caption&&frameExpected){expected=true}return expected}return{setRatio:setRatioClass,setThumbAttr:setThumbAttr,isExpectedCaption:isExpectedCaption}}(UTIL||{},jQuery);function slide($el,options){var elData=$el.data(),elPos=Math.round(options.pos),onEndFn=function(){if(elData&&elData.sliding){elData.sliding=false}(options.onEnd||noop)()};if(typeof options.overPos!=="undefined"&&options.overPos!==options.pos){elPos=options.overPos}var translate=$.extend(getTranslate(elPos,options.direction),options.width&&{width:options.width},options.height&&{height:options.height});if(elData&&elData.sliding){elData.sliding=true}if(CSS3){$el.css($.extend(getDuration(options.time),translate));if(options.time>10){afterTransition($el,"transform",onEndFn,options.time)}else{onEndFn()}}else{$el.stop().animate(translate,options.time,BEZIER,onEndFn)}}function fade($el1,$el2,$frames,options,fadeStack,chain){var chainedFLAG=typeof chain!=="undefined";if(!chainedFLAG){fadeStack.push(arguments);Array.prototype.push.call(arguments,fadeStack.length);if(fadeStack.length>1)return}$el1=$el1||$($el1);$el2=$el2||$($el2);var _$el1=$el1[0],_$el2=$el2[0],crossfadeFLAG=options.method==="crossfade",onEndFn=function(){if(!onEndFn.done){onEndFn.done=true;var args=(chainedFLAG||fadeStack.shift())&&fadeStack.shift();args&&fade.apply(this,args);(options.onEnd||noop)(!!args)}},time=options.time/(chain||1);$frames.removeClass(fadeRearClass+" "+fadeFrontClass);$el1.stop().addClass(fadeRearClass);$el2.stop().addClass(fadeFrontClass);crossfadeFLAG&&_$el2&&$el1.fadeTo(0,0);$el1.fadeTo(crossfadeFLAG?time:0,1,crossfadeFLAG&&onEndFn);$el2.fadeTo(time,0,onEndFn);_$el1&&crossfadeFLAG||_$el2||onEndFn()}var lastEvent,moveEventType,preventEvent,preventEventTimeout,dragDomEl;function extendEvent(e){var touch=(e.touches||[])[0]||e;e._x=touch.pageX||touch.originalEvent.pageX;e._y=touch.clientY||touch.originalEvent.clientY;e._now=$.now()}function touch($el,options){var el=$el[0],tail={},touchEnabledFLAG,startEvent,$target,controlTouch,touchFLAG,targetIsSelectFLAG,targetIsLinkFlag,tolerance,moved;function onStart(e){$target=$(e.target);tail.checked=targetIsSelectFLAG=targetIsLinkFlag=moved=false;if(touchEnabledFLAG||tail.flow||e.touches&&e.touches.length>1||e.which>1||lastEvent&&lastEvent.type!==e.type&&preventEvent||(targetIsSelectFLAG=options.select&&$target.is(options.select,el)))return targetIsSelectFLAG;touchFLAG=e.type==="touchstart";targetIsLinkFlag=$target.is("a, a *",el);controlTouch=tail.control;tolerance=tail.noMove||tail.noSwipe||controlTouch?16:!tail.snap?4:0;extendEvent(e);startEvent=lastEvent=e;moveEventType=e.type.replace(/down|start/,"move").replace(/Down/,"Move");(options.onStart||noop).call(el,e,{control:controlTouch,$target:$target});touchEnabledFLAG=tail.flow=true;if(!touchFLAG||tail.go)stopEvent(e)}function onMove(e){if(e.touches&&e.touches.length>1||MS_POINTER&&!e.isPrimary||moveEventType!==e.type||!touchEnabledFLAG){touchEnabledFLAG&&onEnd();(options.onTouchEnd||noop)();return}extendEvent(e);var xDiff=Math.abs(e._x-startEvent._x),yDiff=Math.abs(e._y-startEvent._y),xyDiff=xDiff-yDiff,xWin=(tail.go||tail.x||xyDiff>=0)&&!tail.noSwipe,yWin=xyDiff<0;if(touchFLAG&&!tail.checked){if(touchEnabledFLAG=xWin){stopEvent(e)}}else{stopEvent(e);if(movedEnough(xDiff,yDiff)){(options.onMove||noop).call(el,e,{touch:touchFLAG})}}if(!moved&&movedEnough(xDiff,yDiff)&&Math.sqrt(Math.pow(xDiff,2)+Math.pow(yDiff,2))>tolerance){moved=true}tail.checked=tail.checked||xWin||yWin}function movedEnough(xDiff,yDiff){return xDiff>yDiff&&xDiff>1.5}function onEnd(e){(options.onTouchEnd||noop)();var _touchEnabledFLAG=touchEnabledFLAG;tail.control=touchEnabledFLAG=false;if(_touchEnabledFLAG){tail.flow=false}if(!_touchEnabledFLAG||targetIsLinkFlag&&!tail.checked)return;e&&stopEvent(e);preventEvent=true;clearTimeout(preventEventTimeout);preventEventTimeout=setTimeout(function(){preventEvent=false},1e3);(options.onEnd||noop).call(el,{moved:moved,$target:$target,control:controlTouch,touch:touchFLAG,startEvent:startEvent,aborted:!e||e.type==="MSPointerCancel"})}function onOtherStart(){if(tail.flow)return;tail.flow=true}function onOtherEnd(){if(!tail.flow)return;tail.flow=false}if(MS_POINTER){addEvent(el,"MSPointerDown",onStart);addEvent(document,"MSPointerMove",onMove);addEvent(document,"MSPointerCancel",onEnd);addEvent(document,"MSPointerUp",onEnd)}else{addEvent(el,"touchstart",onStart);addEvent(el,"touchmove",onMove);addEvent(el,"touchend",onEnd);addEvent(document,"touchstart",onOtherStart);addEvent(document,"touchend",onOtherEnd);addEvent(document,"touchcancel",onOtherEnd);$WINDOW.on("scroll",onOtherEnd);$el.on("mousedown pointerdown",onStart);$DOCUMENT.on("mousemove pointermove",onMove).on("mouseup pointerup",onEnd)}if(Modernizr.touch){dragDomEl="a"}else{dragDomEl="div"}$el.on("click",dragDomEl,function(e){tail.checked&&stopEvent(e)});return tail}function moveOnTouch($el,options){var el=$el[0],elData=$el.data(),tail={},startCoo,coo,startElPos,moveElPos,edge,moveTrack,startTime,endTime,min,max,snap,dir,slowFLAG,controlFLAG,moved,tracked;function startTracking(e,noStop){tracked=true;startCoo=coo=dir==="vertical"?e._y:e._x;startTime=e._now;moveTrack=[[startTime,startCoo]];startElPos=moveElPos=tail.noMove||noStop?0:stop($el,(options.getPos||noop)());(options.onStart||noop).call(el,e)}function onStart(e,result){min=tail.min;max=tail.max;snap=tail.snap,dir=tail.direction||"horizontal",$el.navdir=dir;slowFLAG=e.altKey;tracked=moved=false;controlFLAG=result.control;if(!controlFLAG&&!elData.sliding){startTracking(e)}}function onMove(e,result){if(!tail.noSwipe){if(!tracked){startTracking(e)}coo=dir==="vertical"?e._y:e._x;moveTrack.push([e._now,coo]);moveElPos=startElPos-(startCoo-coo);edge=findShadowEdge(moveElPos,min,max,dir);if(moveElPos<=min){moveElPos=edgeResistance(moveElPos,min)}else if(moveElPos>=max){moveElPos=edgeResistance(moveElPos,max)}if(!tail.noMove){$el.css(getTranslate(moveElPos,dir));if(!moved){moved=true;result.touch||MS_POINTER||$el.addClass(grabbingClass)}(options.onMove||noop).call(el,e,{pos:moveElPos,edge:edge})}}}function onEnd(result){if(tail.noSwipe&&result.moved)return;if(!tracked){startTracking(result.startEvent,true)}result.touch||MS_POINTER||$el.removeClass(grabbingClass);endTime=$.now();var _backTimeIdeal=endTime-TOUCH_TIMEOUT,_backTime,_timeDiff,_timeDiffLast,backTime=null,backCoo,virtualPos,limitPos,newPos,overPos,time=TRANSITION_DURATION,speed,friction=options.friction;for(var _i=moveTrack.length-1;_i>=0;_i--){_backTime=moveTrack[_i][0];_timeDiff=Math.abs(_backTime-_backTimeIdeal);if(backTime===null||_timeDiff<_timeDiffLast){backTime=_backTime;backCoo=moveTrack[_i][1]}else if(backTime===_backTimeIdeal||_timeDiff>_timeDiffLast){break}_timeDiffLast=_timeDiff}newPos=minMaxLimit(moveElPos,min,max);var cooDiff=backCoo-coo,forwardFLAG=cooDiff>=0,timeDiff=endTime-backTime,longTouchFLAG=timeDiff>TOUCH_TIMEOUT,swipeFLAG=!longTouchFLAG&&moveElPos!==startElPos&&newPos===moveElPos;if(snap){newPos=minMaxLimit(Math[swipeFLAG?forwardFLAG?"floor":"ceil":"round"](moveElPos/snap)*snap,min,max);min=max=newPos}if(swipeFLAG&&(snap||newPos===moveElPos)){speed=-(cooDiff/timeDiff);time*=minMaxLimit(Math.abs(speed),options.timeLow,options.timeHigh);virtualPos=Math.round(moveElPos+speed*time/friction);if(!snap){newPos=virtualPos}if(!forwardFLAG&&virtualPos>max||forwardFLAG&&virtualPos"),$anchor=$(div(hiddenClass)),$wrap=$fotorama.find(cls(wrapClass)),$stage=$wrap.find(cls(stageClass)),stage=$stage[0],$stageShaft=$fotorama.find(cls(stageShaftClass)),$stageFrame=$(),$arrPrev=$fotorama.find(cls(arrPrevClass)),$arrNext=$fotorama.find(cls(arrNextClass)),$arrs=$fotorama.find(cls(arrClass)),$navWrap=$fotorama.find(cls(navWrapClass)),$nav=$navWrap.find(cls(navClass)),$navShaft=$nav.find(cls(navShaftClass)),$navFrame,$navDotFrame=$(),$navThumbFrame=$(),stageShaftData=$stageShaft.data(),navShaftData=$navShaft.data(),$thumbBorder=$fotorama.find(cls(thumbBorderClass)),$thumbArrLeft=$fotorama.find(cls(thumbArrLeft)),$thumbArrRight=$fotorama.find(cls(thumbArrRight)),$fullscreenIcon=$fotorama.find(cls(fullscreenIconClass)),fullscreenIcon=$fullscreenIcon[0],$videoPlay=$(div(videoPlayClass)),$videoClose=$fotorama.find(cls(videoCloseClass)),videoClose=$videoClose[0],$spinner=$fotorama.find(cls(fotoramaSpinnerClass)),$videoPlaying,activeIndex=false,activeFrame,activeIndexes,repositionIndex,dirtyIndex,lastActiveIndex,prevIndex,nextIndex,nextAutoplayIndex,startIndex,o_loop,o_nav,o_navThumbs,o_navTop,o_allowFullScreen,o_nativeFullScreen,o_fade,o_thumbSide,o_thumbSide2,o_transitionDuration,o_transition,o_shadows,o_rtl,o_keyboard,lastOptions={},measures={},measuresSetFLAG,stageShaftTouchTail={},stageWheelTail={},navShaftTouchTail={},navWheelTail={},scrollTop,scrollLeft,showedFLAG,pausedAutoplayFLAG,stoppedAutoplayFLAG,toDeactivate={},toDetach={},measuresStash,touchedFLAG,hoverFLAG,navFrameKey,stageLeft=0,fadeStack=[];$wrap[STAGE_FRAME_KEY]=$('
');$wrap[NAV_THUMB_FRAME_KEY]=$($.Fotorama.jst.thumb());$wrap[NAV_DOT_FRAME_KEY]=$($.Fotorama.jst.dots());toDeactivate[STAGE_FRAME_KEY]=[];toDeactivate[NAV_THUMB_FRAME_KEY]=[];toDeactivate[NAV_DOT_FRAME_KEY]=[];toDetach[STAGE_FRAME_KEY]={};$wrap.addClass(CSS3?wrapCss3Class:wrapCss2Class);fotoramaData.fotorama=this;function checkForVideo(){$.each(data,function(i,dataFrame){if(!dataFrame.i){dataFrame.i=dataFrameCount++;var video=findVideoId(dataFrame.video,true);if(video){var thumbs={};dataFrame.video=video;if(!dataFrame.img&&!dataFrame.thumb){thumbs=getVideoThumbs(dataFrame,data,that)}else{dataFrame.thumbsReady=true}updateData(data,{img:thumbs.img,thumb:thumbs.thumb},dataFrame.i,that)}}})}function allowKey(key){return o_keyboard[key]}function setStagePosition(){if($stage!==undefined){if(opts.navdir=="vertical"){var padding=opts.thumbwidth+opts.thumbmargin;$stage.css("left",padding);$arrNext.css("right",padding);$fullscreenIcon.css("right",padding);$wrap.css("width",$wrap.css("width")+padding);$stageShaft.css("max-width",$wrap.width()-padding)}else{$stage.css("left","");$arrNext.css("right","");$fullscreenIcon.css("right","");$wrap.css("width",$wrap.css("width")+padding);$stageShaft.css("max-width","")}}}function bindGlobalEvents(FLAG){var keydownCommon="keydown."+_fotoramaClass,localStamp=_fotoramaClass+stamp,keydownLocal="keydown."+localStamp,keyupLocal="keyup."+localStamp,resizeLocal="resize."+localStamp+" "+"orientationchange."+localStamp,showParams;if(FLAG){$DOCUMENT.on(keydownLocal,function(e){var catched,index;if($videoPlaying&&e.keyCode===27){catched=true;unloadVideo($videoPlaying,true,true)}else if(that.fullScreen||opts.keyboard&&!that.index){if(e.keyCode===27){catched=true;that.cancelFullScreen()}else if(e.shiftKey&&e.keyCode===32&&allowKey("space")||!e.altKey&&!e.metaKey&&e.keyCode===37&&allowKey("left")||e.keyCode===38&&allowKey("up")&&$(":focus").attr("data-gallery-role")){that.longPress.progress();index="<"}else if(e.keyCode===32&&allowKey("space")||!e.altKey&&!e.metaKey&&e.keyCode===39&&allowKey("right")||e.keyCode===40&&allowKey("down")&&$(":focus").attr("data-gallery-role")){that.longPress.progress();index=">"}else if(e.keyCode===36&&allowKey("home")){that.longPress.progress();index="<<"}else if(e.keyCode===35&&allowKey("end")){that.longPress.progress();index=">>"}}(catched||index)&&stopEvent(e);showParams={index:index,slow:e.altKey,user:true};index&&(that.longPress.inProgress?that.showWhileLongPress(showParams):that.show(showParams))});if(FLAG){$DOCUMENT.on(keyupLocal,function(e){if(that.longPress.inProgress){that.showEndLongPress({user:true})}that.longPress.reset()})}if(!that.index){$DOCUMENT.off(keydownCommon).on(keydownCommon,"textarea, input, select",function(e){!$BODY.hasClass(_fullscreenClass)&&e.stopPropagation()})}$WINDOW.on(resizeLocal,that.resize)}else{$DOCUMENT.off(keydownLocal);$WINDOW.off(resizeLocal)}}function appendElements(FLAG){if(FLAG===appendElements.f)return;if(FLAG){$fotorama.addClass(_fotoramaClass+" "+stampClass).before($anchor).before($style);addInstance(that)}else{$anchor.detach();$style.detach();$fotorama.html(fotoramaData.urtext).removeClass(stampClass);hideInstance(that)}bindGlobalEvents(FLAG);appendElements.f=FLAG}function setData(){data=that.data=data||clone(opts.data)||getDataFromHtml($fotorama);size=that.size=data.length;ready.ok&&opts.shuffle&&shuffle(data);checkForVideo();activeIndex=limitIndex(activeIndex);size&&appendElements(true)}function stageNoMove(){var _noMove=size<2||$videoPlaying;stageShaftTouchTail.noMove=_noMove||o_fade;stageShaftTouchTail.noSwipe=_noMove||!opts.swipe;!o_transition&&$stageShaft.toggleClass(grabClass,!opts.click&&!stageShaftTouchTail.noMove&&!stageShaftTouchTail.noSwipe);MS_POINTER&&$wrap.toggleClass(wrapPanYClass,!stageShaftTouchTail.noSwipe)}function setAutoplayInterval(interval){if(interval===true)interval="";opts.autoplay=Math.max(+interval||AUTOPLAY_INTERVAL,o_transitionDuration*1.5)}function updateThumbArrow(opt){if(opt.navarrows&&opt.nav==="thumbs"){$thumbArrLeft.show();$thumbArrRight.show()}else{$thumbArrLeft.hide();$thumbArrRight.hide()}}function getThumbsInSlide($el,opts){return Math.floor($wrap.width()/(opts.thumbwidth+opts.thumbmargin))}function setOptions(){if(!opts.nav||opts.nav==="dots"){opts.navdir="horizontal"}that.options=opts=optionsToLowerCase(opts);thumbsPerSlide=getThumbsInSlide($wrap,opts);o_fade=opts.transition==="crossfade"||opts.transition==="dissolve";o_loop=opts.loop&&(size>2||o_fade&&(!o_transition||o_transition!=="slide"));o_transitionDuration=+opts.transitionduration||TRANSITION_DURATION;o_rtl=opts.direction==="rtl";o_keyboard=$.extend({},opts.keyboard&&KEYBOARD_OPTIONS,opts.keyboard);updateThumbArrow(opts);var classes={add:[],remove:[]};function addOrRemoveClass(FLAG,value){classes[FLAG?"add":"remove"].push(value)}if(size>1){o_nav=opts.nav;o_navTop=opts.navposition==="top";classes.remove.push(selectClass);$arrs.toggle(!!opts.arrows)}else{o_nav=false;$arrs.hide()}arrsUpdate();stageWheelUpdate();thumbArrUpdate();if(opts.autoplay)setAutoplayInterval(opts.autoplay);o_thumbSide=numberFromMeasure(opts.thumbwidth)||THUMB_SIZE;o_thumbSide2=numberFromMeasure(opts.thumbheight)||THUMB_SIZE;stageWheelTail.ok=navWheelTail.ok=opts.trackpad&&!SLOW;stageNoMove();extendMeasures(opts,[measures]);o_navThumbs=o_nav==="thumbs";if($navWrap.filter(":hidden")&&!!o_nav){$navWrap.show()}if(o_navThumbs){frameDraw(size,"navThumb");$navFrame=$navThumbFrame;navFrameKey=NAV_THUMB_FRAME_KEY;setStyle($style,$.Fotorama.jst.style({w:o_thumbSide,h:o_thumbSide2,b:opts.thumbborderwidth,m:opts.thumbmargin,s:stamp,q:!COMPAT}));$nav.addClass(navThumbsClass).removeClass(navDotsClass)}else if(o_nav==="dots"){frameDraw(size,"navDot");$navFrame=$navDotFrame;navFrameKey=NAV_DOT_FRAME_KEY;$nav.addClass(navDotsClass).removeClass(navThumbsClass)}else{$navWrap.hide();o_nav=false;$nav.removeClass(navThumbsClass+" "+navDotsClass)}if(o_nav){if(o_navTop){$navWrap.insertBefore($stage)}else{$navWrap.insertAfter($stage)}frameAppend.nav=false;frameAppend($navFrame,$navShaft,"nav")}o_allowFullScreen=opts.allowfullscreen;if(o_allowFullScreen){$fullscreenIcon.prependTo($stage);o_nativeFullScreen=FULLSCREEN&&o_allowFullScreen==="native";stubEvent($fullscreenIcon,"touchend")}else{$fullscreenIcon.detach();o_nativeFullScreen=false}addOrRemoveClass(o_fade,wrapFadeClass);addOrRemoveClass(!o_fade,wrapSlideClass);addOrRemoveClass(!opts.captions,wrapNoCaptionsClass);addOrRemoveClass(o_rtl,wrapRtlClass);addOrRemoveClass(opts.arrows,wrapToggleArrowsClass);o_shadows=opts.shadows&&!SLOW;addOrRemoveClass(!o_shadows,wrapNoShadowsClass);$wrap.addClass(classes.add.join(" ")).removeClass(classes.remove.join(" "));lastOptions=$.extend({},opts);setStagePosition()}function normalizeIndex(index){return index<0?(size+index%size)%size:index>=size?index%size:index}function limitIndex(index){return minMaxLimit(index,0,size-1)}function edgeIndex(index){return o_loop?normalizeIndex(index):limitIndex(index)}function getPrevIndex(index){return index>0||o_loop?index-1:false}function getNextIndex(index){return index1&&data[index]===dataFrame&&!dataFrame.html&&!dataFrame.deleted&&!dataFrame.video&&!fullFLAG){dataFrame.deleted=true;that.splice(index,1)}}}function loaded(){$.Fotorama.measures[src]=imgData.measures=$.Fotorama.measures[src]||{width:img.width,height:img.height,ratio:img.width/img.height};setMeasures(imgData.measures.width,imgData.measures.height,imgData.measures.ratio,index);$img.off("load error").addClass(""+(fullFLAG?imgFullClass:imgClass)).attr("aria-hidden","false").prependTo($frame);if($frame.hasClass(stageFrameClass)&&!$frame.hasClass(videoContainerClass)){$frame.attr("href",$img.attr("src"))}fit($img,($.isFunction(specialMeasures)?specialMeasures():specialMeasures)||measures);$.Fotorama.cache[src]=frameData.state="loaded";setTimeout(function(){$frame.trigger("f:load").removeClass(loadingClass+" "+errorClass).addClass(loadedClass+" "+(fullFLAG?loadedFullClass:loadedImgClass));if(type==="stage"){triggerTriggerEvent("load")}else if(dataFrame.thumbratio===AUTO||!dataFrame.thumbratio&&opts.thumbratio===AUTO){dataFrame.thumbratio=imgData.measures.ratio;reset()}},0)}if(!src){error();return}function waitAndLoad(){var _i=10;waitFor(function(){return!touchedFLAG||!_i--&&!SLOW},function(){loaded()})}if(!$.Fotorama.cache[src]){$.Fotorama.cache[src]="*";$img.on("load",waitAndLoad).on("error",error)}else{(function justWait(){if($.Fotorama.cache[src]==="error"){error()}else if($.Fotorama.cache[src]==="loaded"){setTimeout(waitAndLoad,0)}else{setTimeout(justWait,100)}})()}frameData.state="";img.src=src;if(frameData.data.caption){img.alt=frameData.data.caption||""}if(frameData.data.full){$(img).data("original",frameData.data.full)}if(UTIL.isExpectedCaption(dataFrame,opts.showcaption)){$(img).attr("aria-labelledby",dataFrame.labelledby)}})}function updateFotoramaState(){var $frame=activeFrame[STAGE_FRAME_KEY];if($frame&&!$frame.data().state){$spinner.addClass(spinnerShowClass);$frame.on("f:load f:error",function(){$frame.off("f:load f:error");$spinner.removeClass(spinnerShowClass)})}}function addNavFrameEvents(frame){addEnterUp(frame,onNavFrameClick);addFocus(frame,function(){setTimeout(function(){lockScroll($nav)},0);slideNavShaft({time:o_transitionDuration,guessIndex:$(this).data().eq,minMax:navShaftTouchTail})})}function frameDraw(indexes,type){eachIndex(indexes,type,function(i,index,dataFrame,$frame,key,frameData){if($frame)return;$frame=dataFrame[key]=$wrap[key].clone();frameData=$frame.data();frameData.data=dataFrame;var frame=$frame[0],labelledbyValue="labelledby"+$.now();if(type==="stage"){if(dataFrame.html){$('
').append(dataFrame._html?$(dataFrame.html).removeAttr("id").html(dataFrame._html):dataFrame.html).appendTo($frame)}if(dataFrame.id){labelledbyValue=dataFrame.id||labelledbyValue}dataFrame.labelledby=labelledbyValue;if(UTIL.isExpectedCaption(dataFrame,opts.showcaption)){$($.Fotorama.jst.frameCaption({caption:dataFrame.caption,labelledby:labelledbyValue})).appendTo($frame)}dataFrame.video&&$frame.addClass(stageFrameVideoClass).append($videoPlay.clone());addFocus(frame,function(){setTimeout(function(){lockScroll($stage)},0);clickToShow({index:frameData.eq,user:true})});$stageFrame=$stageFrame.add($frame)}else if(type==="navDot"){addNavFrameEvents(frame);$navDotFrame=$navDotFrame.add($frame)}else if(type==="navThumb"){addNavFrameEvents(frame);frameData.$wrap=$frame.children(":first");$navThumbFrame=$navThumbFrame.add($frame);if(dataFrame.video){frameData.$wrap.append($videoPlay.clone())}}})}function callFit($img,measuresToFit){return $img&&$img.length&&fit($img,measuresToFit)}function stageFramePosition(indexes){eachIndex(indexes,"stage",function(i,index,dataFrame,$frame,key,frameData){if(!$frame)return;var normalizedIndex=normalizeIndex(index);frameData.eq=normalizedIndex;toDetach[STAGE_FRAME_KEY][normalizedIndex]=$frame.css($.extend({left:o_fade?0:getPosByIndex(index,measures.w,opts.margin,repositionIndex)},o_fade&&getDuration(0)));if(isDetached($frame[0])){$frame.appendTo($stageShaft);unloadVideo(dataFrame.$video)}callFit(frameData.$img,measures);callFit(frameData.$full,measures);if($frame.hasClass(stageFrameClass)&&!($frame.attr("aria-hidden")==="false"&&$frame.hasClass(activeClass))){$frame.attr("aria-hidden","true")}})}function thumbsDraw(pos,loadFLAG){var leftLimit,rightLimit,exceedLimit;if(o_nav!=="thumbs"||isNaN(pos))return;leftLimit=-pos;rightLimit=-pos+measures.nw;if(opts.navdir==="vertical"){pos=pos-opts.thumbheight;rightLimit=-pos+measures.h}$navThumbFrame.each(function(){var $this=$(this),thisData=$this.data(),eq=thisData.eq,getSpecialMeasures=function(){return{h:o_thumbSide2,w:thisData.w}},specialMeasures=getSpecialMeasures(),exceedLimit=opts.navdir==="vertical"?thisData.t>rightLimit:thisData.l>rightLimit;specialMeasures.w=thisData.w;if(thisData.l+thisData.wmeasures.w/3}function disableDirrection(i){return!o_loop&&(!(activeIndex+i)||!(activeIndex-size+i))&&!$videoPlaying}function arrsUpdate(){var disablePrev=disableDirrection(0),disableNext=disableDirrection(1);$arrPrev.toggleClass(arrDisabledClass,disablePrev).attr(disableAttr(disablePrev,false));$arrNext.toggleClass(arrDisabledClass,disableNext).attr(disableAttr(disableNext,false))}function thumbArrUpdate(){var isLeftDisable=false,isRightDisable=false;if(opts.navtype==="thumbs"&&!opts.loop){activeIndex==0?isLeftDisable=true:isLeftDisable=false;activeIndex==opts.data.length-1?isRightDisable=true:isRightDisable=false}if(opts.navtype==="slides"){var pos=readPosition($navShaft,opts.navdir);pos>=navShaftTouchTail.max?isLeftDisable=true:isLeftDisable=false;pos<=navShaftTouchTail.min?isRightDisable=true:isRightDisable=false}$thumbArrLeft.toggleClass(arrDisabledClass,isLeftDisable).attr(disableAttr(isLeftDisable,true));$thumbArrRight.toggleClass(arrDisabledClass,isRightDisable).attr(disableAttr(isRightDisable,true))}function stageWheelUpdate(){if(stageWheelTail.ok){stageWheelTail.prevent={"<":disableDirrection(0),">":disableDirrection(1)}}}function getNavFrameBounds($navFrame){var navFrameData=$navFrame.data(),left,top,width,height;if(o_navThumbs){left=navFrameData.l;top=navFrameData.t;width=navFrameData.w;height=navFrameData.h}else{left=$navFrame.position().left;width=$navFrame.width()}var horizontalBounds={c:left+width/2,min:-left+opts.thumbmargin*10,max:-left+measures.w-width-opts.thumbmargin*10};var verticalBounds={c:top+height/2,min:-top+opts.thumbmargin*10,max:-top+measures.h-height-opts.thumbmargin*10};return opts.navdir==="vertical"?verticalBounds:horizontalBounds}function slideThumbBorder(time){var navFrameData=activeFrame[navFrameKey].data();slide($thumbBorder,{time:time*1.2,pos:opts.navdir==="vertical"?navFrameData.t:navFrameData.l,width:navFrameData.w,height:navFrameData.h,direction:opts.navdir})}function slideNavShaft(options){var $guessNavFrame=data[options.guessIndex][navFrameKey],typeOfAnimation=opts.navtype;var overflowFLAG,time,minMax,boundTop,boundLeft,l,pos,x;if($guessNavFrame){if(typeOfAnimation==="thumbs"){overflowFLAG=navShaftTouchTail.min!==navShaftTouchTail.max;minMax=options.minMax||overflowFLAG&&getNavFrameBounds(activeFrame[navFrameKey]);boundTop=overflowFLAG&&(options.keep&&slideNavShaft.t?slideNavShaft.l:minMaxLimit((options.coo||measures.nw/2)-getNavFrameBounds($guessNavFrame).c,minMax.min,minMax.max));boundLeft=overflowFLAG&&(options.keep&&slideNavShaft.l?slideNavShaft.l:minMaxLimit((options.coo||measures.nw/2)-getNavFrameBounds($guessNavFrame).c,minMax.min,minMax.max));l=opts.navdir==="vertical"?boundTop:boundLeft;pos=overflowFLAG&&minMaxLimit(l,navShaftTouchTail.min,navShaftTouchTail.max)||0;time=options.time*1.1;slide($navShaft,{time:time,pos:pos,direction:opts.navdir,onEnd:function(){thumbsDraw(pos,true);thumbArrUpdate()}});setShadow($nav,findShadowEdge(pos,navShaftTouchTail.min,navShaftTouchTail.max,opts.navdir));slideNavShaft.l=l}else{x=readPosition($navShaft,opts.navdir);time=options.time*1.11;pos=validateSlidePos(opts,navShaftTouchTail,options.guessIndex,x,$guessNavFrame,$navWrap,opts.navdir);slide($navShaft,{time:time,pos:pos,direction:opts.navdir,onEnd:function(){thumbsDraw(pos,true);thumbArrUpdate()}});setShadow($nav,findShadowEdge(pos,navShaftTouchTail.min,navShaftTouchTail.max,opts.navdir))}}}function navUpdate(){deactivateFrames(navFrameKey);toDeactivate[navFrameKey].push(activeFrame[navFrameKey].addClass(activeClass).attr("data-active",true))}function deactivateFrames(key){var _toDeactivate=toDeactivate[key];while(_toDeactivate.length){_toDeactivate.shift().removeClass(activeClass).attr("data-active",false)}}function detachFrames(key){var _toDetach=toDetach[key];$.each(activeIndexes,function(i,index){delete _toDetach[normalizeIndex(index)]});$.each(_toDetach,function(index,$frame){delete _toDetach[index];$frame.detach()})}function stageShaftReposition(skipOnEnd){repositionIndex=dirtyIndex=activeIndex;var $frame=activeFrame[STAGE_FRAME_KEY];if($frame){deactivateFrames(STAGE_FRAME_KEY);toDeactivate[STAGE_FRAME_KEY].push($frame.addClass(activeClass).attr("data-active",true));if($frame.hasClass(stageFrameClass)){$frame.attr("aria-hidden","false")}skipOnEnd||that.showStage.onEnd(true);stop($stageShaft,0,true);detachFrames(STAGE_FRAME_KEY);stageFramePosition(activeIndexes);setStageShaftMinmaxAndSnap();setNavShaftMinMax();addEnterUp($stageShaft[0],function(){if(!$fotorama.hasClass(fullscreenClass)){that.requestFullScreen();$fullscreenIcon.focus()}})}}function extendMeasures(options,measuresArray){if(!options)return;$.each(measuresArray,function(i,measures){if(!measures)return;$.extend(measures,{width:options.width||measures.width,height:options.height,minwidth:options.minwidth,maxwidth:options.maxwidth,minheight:options.minheight,maxheight:options.maxheight,ratio:getRatio(options.ratio)})})}function triggerEvent(event,extra){$fotorama.trigger(_fotoramaClass+":"+event,[that,extra])}function onTouchStart(){clearTimeout(onTouchEnd.t);touchedFLAG=1;if(opts.stopautoplayontouch){that.stopAutoplay()}else{pausedAutoplayFLAG=true}}function onTouchEnd(){if(!touchedFLAG)return;if(!opts.stopautoplayontouch){releaseAutoplay();changeAutoplay()}onTouchEnd.t=setTimeout(function(){touchedFLAG=0},TRANSITION_DURATION+TOUCH_TIMEOUT)}function releaseAutoplay(){pausedAutoplayFLAG=!!($videoPlaying||stoppedAutoplayFLAG)}function changeAutoplay(){clearTimeout(changeAutoplay.t);waitFor.stop(changeAutoplay.w);if(!opts.autoplay||pausedAutoplayFLAG){if(that.autoplay){that.autoplay=false;triggerEvent("stopautoplay")}return}if(!that.autoplay){that.autoplay=true;triggerEvent("startautoplay")}var _activeIndex=activeIndex;var frameData=activeFrame[STAGE_FRAME_KEY].data();changeAutoplay.w=waitFor(function(){return frameData.state||_activeIndex!==activeIndex},function(){changeAutoplay.t=setTimeout(function(){if(pausedAutoplayFLAG||_activeIndex!==activeIndex)return;var _nextAutoplayIndex=nextAutoplayIndex,nextFrameData=data[_nextAutoplayIndex][STAGE_FRAME_KEY].data();changeAutoplay.w=waitFor(function(){return nextFrameData.state||_nextAutoplayIndex!==nextAutoplayIndex},function(){if(pausedAutoplayFLAG||_nextAutoplayIndex!==nextAutoplayIndex)return;that.show(o_loop?getDirectionSign(!o_rtl):nextAutoplayIndex)})},opts.autoplay)})}that.startAutoplay=function(interval){if(that.autoplay)return this;pausedAutoplayFLAG=stoppedAutoplayFLAG=false;setAutoplayInterval(interval||opts.autoplay);changeAutoplay();return this};that.stopAutoplay=function(){if(that.autoplay){pausedAutoplayFLAG=stoppedAutoplayFLAG=true;changeAutoplay()}return this};that.showSlide=function(slideDir){var currentPosition=readPosition($navShaft,opts.navdir),pos,time=500*1.1,size=opts.navdir==="horizontal"?opts.thumbwidth:opts.thumbheight,onEnd=function(){thumbArrUpdate()};if(slideDir==="next"){pos=currentPosition-(size+opts.margin)*thumbsPerSlide}if(slideDir==="prev"){pos=currentPosition+(size+opts.margin)*thumbsPerSlide}pos=validateRestrictions(pos,navShaftTouchTail);thumbsDraw(pos,true);slide($navShaft,{time:time,pos:pos,direction:opts.navdir,onEnd:onEnd})};that.showWhileLongPress=function(options){if(that.longPress.singlePressInProgress){return}var index=calcActiveIndex(options);calcGlobalIndexes(index);var time=calcTime(options)/50;var _activeFrame=activeFrame;that.activeFrame=activeFrame=data[activeIndex];var silent=_activeFrame===activeFrame&&!options.user;that.showNav(silent,options,time);return this};that.showEndLongPress=function(options){if(that.longPress.singlePressInProgress){return}var index=calcActiveIndex(options);calcGlobalIndexes(index);var time=calcTime(options)/50;var _activeFrame=activeFrame;that.activeFrame=activeFrame=data[activeIndex];var silent=_activeFrame===activeFrame&&!options.user;that.showStage(silent,options,time);showedFLAG=typeof lastActiveIndex!=="undefined"&&lastActiveIndex!==activeIndex;lastActiveIndex=activeIndex;return this};function calcActiveIndex(options){var index;if(typeof options!=="object"){index=options;options={}}else{index=options.index}index=index===">"?dirtyIndex+1:index==="<"?dirtyIndex-1:index==="<<"?0:index===">>"?size-1:index;index=isNaN(index)?undefined:index;index=typeof index==="undefined"?activeIndex||0:index;return index}function calcGlobalIndexes(index){that.activeIndex=activeIndex=edgeIndex(index);prevIndex=getPrevIndex(activeIndex);nextIndex=getNextIndex(activeIndex);nextAutoplayIndex=normalizeIndex(activeIndex+(o_rtl?-1:1));activeIndexes=[activeIndex,prevIndex,nextIndex];dirtyIndex=o_loop?index:activeIndex}function calcTime(options){var diffIndex=Math.abs(lastActiveIndex-dirtyIndex),time=getNumber(options.time,function(){return Math.min(o_transitionDuration*(1+(diffIndex-1)/12),o_transitionDuration*2)});if(options.slow){time*=10}return time}that.showStage=function(silent,options,time){unloadVideo($videoPlaying,activeFrame.i!==data[normalizeIndex(repositionIndex)].i);frameDraw(activeIndexes,"stage");stageFramePosition(SLOW?[dirtyIndex]:[dirtyIndex,getPrevIndex(dirtyIndex),getNextIndex(dirtyIndex)]);updateTouchTails("go",true);silent||triggerEvent("show",{user:options.user,time:time});pausedAutoplayFLAG=true;var overPos=options.overPos;var onEnd=that.showStage.onEnd=function(skipReposition){if(onEnd.ok)return;onEnd.ok=true;skipReposition||stageShaftReposition(true);if(!silent){triggerEvent("showend",{user:options.user})}if(!skipReposition&&o_transition&&o_transition!==opts.transition){that.setOptions({transition:o_transition});o_transition=false;return}updateFotoramaState();loadImg(activeIndexes,"stage");updateTouchTails("go",false);stageWheelUpdate();stageCursor();releaseAutoplay();changeAutoplay();if(that.fullScreen){activeFrame[STAGE_FRAME_KEY].find("."+imgFullClass).attr("aria-hidden",false);activeFrame[STAGE_FRAME_KEY].find("."+imgClass).attr("aria-hidden",true)}else{activeFrame[STAGE_FRAME_KEY].find("."+imgFullClass).attr("aria-hidden",true);activeFrame[STAGE_FRAME_KEY].find("."+imgClass).attr("aria-hidden",false)}};if(!o_fade){slide($stageShaft,{pos:-getPosByIndex(dirtyIndex,measures.w,opts.margin,repositionIndex),overPos:overPos,time:time,onEnd:onEnd})}else{var $activeFrame=activeFrame[STAGE_FRAME_KEY],$prevActiveFrame=data[lastActiveIndex]&&activeIndex!==lastActiveIndex?data[lastActiveIndex][STAGE_FRAME_KEY]:null;fade($activeFrame,$prevActiveFrame,$stageFrame,{time:time,method:opts.transition,onEnd:onEnd},fadeStack)}arrsUpdate()};that.showNav=function(silent,options,time){thumbArrUpdate();if(o_nav){navUpdate();var guessIndex=limitIndex(activeIndex+minMaxLimit(dirtyIndex-lastActiveIndex,-1,1));slideNavShaft({time:time,coo:guessIndex!==activeIndex&&options.coo,guessIndex:typeof options.coo!=="undefined"?guessIndex:activeIndex,keep:silent});if(o_navThumbs)slideThumbBorder(time)}};that.show=function(options){that.longPress.singlePressInProgress=true;var index=calcActiveIndex(options);calcGlobalIndexes(index);var time=calcTime(options);var _activeFrame=activeFrame;that.activeFrame=activeFrame=data[activeIndex];var silent=_activeFrame===activeFrame&&!options.user;that.showStage(silent,options,time);that.showNav(silent,options,time);showedFLAG=typeof lastActiveIndex!=="undefined"&&lastActiveIndex!==activeIndex;lastActiveIndex=activeIndex;that.longPress.singlePressInProgress=false;return this};that.requestFullScreen=function(){if(o_allowFullScreen&&!that.fullScreen){var isVideo=$((that.activeFrame||{}).$stageFrame||{}).hasClass("fotorama-video-container");if(isVideo){return}scrollTop=$WINDOW.scrollTop();scrollLeft=$WINDOW.scrollLeft();lockScroll($WINDOW);updateTouchTails("x",true);measuresStash=$.extend({},measures);$fotorama.addClass(fullscreenClass).appendTo($BODY.addClass(_fullscreenClass));$HTML.addClass(_fullscreenClass);unloadVideo($videoPlaying,true,true);that.fullScreen=true;if(o_nativeFullScreen){fullScreenApi.request(fotorama)}that.resize();loadImg(activeIndexes,"stage");updateFotoramaState();triggerEvent("fullscreenenter");if(!("ontouchstart"in window)){$fullscreenIcon.focus()}}return this};function cancelFullScreen(){if(that.fullScreen){that.fullScreen=false;if(FULLSCREEN){fullScreenApi.cancel(fotorama)}$BODY.removeClass(_fullscreenClass);$HTML.removeClass(_fullscreenClass);$fotorama.removeClass(fullscreenClass).insertAfter($anchor);measures=$.extend({},measuresStash);unloadVideo($videoPlaying,true,true);updateTouchTails("x",false);that.resize();loadImg(activeIndexes,"stage");lockScroll($WINDOW,scrollLeft,scrollTop);triggerEvent("fullscreenexit")}}that.cancelFullScreen=function(){if(o_nativeFullScreen&&fullScreenApi.is()){fullScreenApi.cancel(document)}else{cancelFullScreen()}return this};that.toggleFullScreen=function(){return that[(that.fullScreen?"cancel":"request")+"FullScreen"]()};that.resize=function(options){if(!data)return this;var time=arguments[1]||0,setFLAG=arguments[2];thumbsPerSlide=getThumbsInSlide($wrap,opts);extendMeasures(!that.fullScreen?optionsToLowerCase(options):{width:$(window).width(),maxwidth:null,minwidth:null,height:$(window).height(),maxheight:null,minheight:null},[measures,setFLAG||that.fullScreen||opts]);var width=measures.width,height=measures.height,ratio=measures.ratio,windowHeight=$WINDOW.height()-(o_nav?$nav.height():0);if(measureIsValid(width)){$wrap.css({width:""});$wrap.css({height:""});$stage.css({width:""});$stage.css({height:""});$stageShaft.css({width:""});$stageShaft.css({height:""});$nav.css({width:""});$nav.css({height:""});$wrap.css({minWidth:measures.minwidth||0,maxWidth:measures.maxwidth||MAX_WIDTH});if(o_nav==="dots"){$navWrap.hide()}width=measures.W=measures.w=$wrap.width();measures.nw=o_nav&&numberFromWhatever(opts.navwidth,width)||width;$stageShaft.css({width:measures.w,marginLeft:(measures.W-measures.w)/2});height=numberFromWhatever(height,windowHeight);height=height||ratio&&width/ratio;if(height){width=Math.round(width);height=measures.h=Math.round(minMaxLimit(height,numberFromWhatever(measures.minheight,windowHeight),numberFromWhatever(measures.maxheight,windowHeight)));$stage.css({width:width,height:height});if(opts.navdir==="vertical"&&!that.fullscreen){$nav.width(opts.thumbwidth+opts.thumbmargin*2)}if(opts.navdir==="horizontal"&&!that.fullscreen){$nav.height(opts.thumbheight+opts.thumbmargin*2)}if(o_nav==="dots"){$nav.width(width).height("auto");$navWrap.show()}if(opts.navdir==="vertical"&&that.fullScreen){$stage.css("height",$WINDOW.height())}if(opts.navdir==="horizontal"&&that.fullScreen){$stage.css("height",$WINDOW.height()-$nav.height())}if(o_nav){switch(opts.navdir){case"vertical":$navWrap.removeClass(navShafthorizontalClass);$navWrap.removeClass(navShaftListClass);$navWrap.addClass(navShaftVerticalClass);$nav.stop().animate({height:measures.h,width:opts.thumbwidth},time);break;case"list":$navWrap.removeClass(navShaftVerticalClass);$navWrap.removeClass(navShafthorizontalClass);$navWrap.addClass(navShaftListClass);break;default:$navWrap.removeClass(navShaftVerticalClass);$navWrap.removeClass(navShaftListClass);$navWrap.addClass(navShafthorizontalClass);$nav.stop().animate({width:measures.nw},time);break}stageShaftReposition();slideNavShaft({guessIndex:activeIndex,time:time,keep:true});if(o_navThumbs&&frameAppend.nav)slideThumbBorder(time)}measuresSetFLAG=setFLAG||true;ready.ok=true;ready()}}stageLeft=$stage.offset().left;setStagePosition();return this};that.setOptions=function(options){$.extend(opts,options);reset();return this};that.shuffle=function(){data&&shuffle(data)&&reset();return this};function setShadow($el,edge){if(o_shadows){$el.removeClass(shadowsLeftClass+" "+shadowsRightClass);$el.removeClass(shadowsTopClass+" "+shadowsBottomClass);edge&&!$videoPlaying&&$el.addClass(edge.replace(/^|\s/g," "+shadowsClass+"--"))}}that.longPress={threshold:1,count:0,thumbSlideTime:20,progress:function(){if(!this.inProgress){this.count++;this.inProgress=this.count>this.threshold}},end:function(){if(this.inProgress){this.isEnded=true}},reset:function(){this.count=0;this.inProgress=false;this.isEnded=false}};that.destroy=function(){that.cancelFullScreen();that.stopAutoplay();data=that.data=null;appendElements();activeIndexes=[];detachFrames(STAGE_FRAME_KEY);reset.ok=false;return this};that.playVideo=function(){var dataFrame=activeFrame,video=dataFrame.video,_activeIndex=activeIndex;if(typeof video==="object"&&dataFrame.videoReady){o_nativeFullScreen&&that.fullScreen&&that.cancelFullScreen();waitFor(function(){return!fullScreenApi.is()||_activeIndex!==activeIndex},function(){if(_activeIndex===activeIndex){dataFrame.$video=dataFrame.$video||$(div(videoClass)).append(createVideoFrame(video));dataFrame.$video.appendTo(dataFrame[STAGE_FRAME_KEY]);$wrap.addClass(wrapVideoClass);$videoPlaying=dataFrame.$video;stageNoMove();$arrs.blur();$fullscreenIcon.blur();triggerEvent("loadvideo")}})}return this};that.stopVideo=function(){unloadVideo($videoPlaying,true,true);return this};that.spliceByIndex=function(index,newImgObj){newImgObj.i=index+1;newImgObj.img&&$.ajax({url:newImgObj.img,type:"HEAD",success:function(){data.splice(index,1,newImgObj);reset()}})};function unloadVideo($video,unloadActiveFLAG,releaseAutoplayFLAG){if(unloadActiveFLAG){$wrap.removeClass(wrapVideoClass);$videoPlaying=false;stageNoMove()}if($video&&$video!==$videoPlaying){$video.remove();triggerEvent("unloadvideo")}if(releaseAutoplayFLAG){releaseAutoplay();changeAutoplay()}}function toggleControlsClass(FLAG){$wrap.toggleClass(wrapNoControlsClass,FLAG)}function stageCursor(e){if(stageShaftTouchTail.flow)return;var x=e?e.pageX:stageCursor.x,pointerFLAG=x&&!disableDirrection(getDirection(x))&&opts.click;if(stageCursor.p!==pointerFLAG&&$stage.toggleClass(pointerClass,pointerFLAG)){stageCursor.p=pointerFLAG;stageCursor.x=x}}$stage.on("mousemove",stageCursor);function clickToShow(showOptions){clearTimeout(clickToShow.t);if(opts.clicktransition&&opts.clicktransition!==opts.transition){setTimeout(function(){var _o_transition=opts.transition;that.setOptions({transition:opts.clicktransition});o_transition=_o_transition;clickToShow.t=setTimeout(function(){that.show(showOptions)},10)},0)}else{that.show(showOptions)}}function onStageTap(e,toggleControlsFLAG){var target=e.target,$target=$(target);if($target.hasClass(videoPlayClass)){that.playVideo()}else if(target===fullscreenIcon){that.toggleFullScreen()}else if($videoPlaying){target===videoClose&&unloadVideo($videoPlaying,true,true)}else if(!$fotorama.hasClass(fullscreenClass)){that.requestFullScreen()}}function updateTouchTails(key,value){stageShaftTouchTail[key]=navShaftTouchTail[key]=value}stageShaftTouchTail=moveOnTouch($stageShaft,{onStart:onTouchStart,onMove:function(e,result){setShadow($stage,result.edge)},onTouchEnd:onTouchEnd,onEnd:function(result){var toggleControlsFLAG;setShadow($stage);toggleControlsFLAG=(MS_POINTER&&!hoverFLAG||result.touch)&&opts.arrows;if((result.moved||toggleControlsFLAG&&result.pos!==result.newPos&&!result.control)&&result.$target[0]!==$fullscreenIcon[0]){var index=getIndexByPos(result.newPos,measures.w,opts.margin,repositionIndex);that.show({index:index,time:o_fade?o_transitionDuration:result.time,overPos:result.overPos,user:true})}else if(!result.aborted&&!result.control){onStageTap(result.startEvent,toggleControlsFLAG)}},timeLow:1,timeHigh:1,friction:2,select:"."+selectClass+", ."+selectClass+" *",$wrap:$stage,direction:"horizontal"});navShaftTouchTail=moveOnTouch($navShaft,{onStart:onTouchStart,onMove:function(e,result){setShadow($nav,result.edge)},onTouchEnd:onTouchEnd,onEnd:function(result){function onEnd(){slideNavShaft.l=result.newPos;releaseAutoplay();changeAutoplay();thumbsDraw(result.newPos,true);thumbArrUpdate()}if(!result.moved){var target=result.$target.closest("."+navFrameClass,$navShaft)[0];target&&onNavFrameClick.call(target,result.startEvent)}else if(result.pos!==result.newPos){pausedAutoplayFLAG=true;slide($navShaft,{time:result.time,pos:result.newPos,overPos:result.overPos,direction:opts.navdir,onEnd:onEnd});thumbsDraw(result.newPos);o_shadows&&setShadow($nav,findShadowEdge(result.newPos,navShaftTouchTail.min,navShaftTouchTail.max,result.dir))}else{onEnd()}},timeLow:.5,timeHigh:2,friction:5,$wrap:$nav,direction:opts.navdir});stageWheelTail=wheel($stage,{shift:true,onEnd:function(e,direction){onTouchStart();onTouchEnd();that.show({index:direction,slow:e.altKey})}});navWheelTail=wheel($nav,{onEnd:function(e,direction){onTouchStart();onTouchEnd();var newPos=stop($navShaft)+direction*.25;$navShaft.css(getTranslate(minMaxLimit(newPos,navShaftTouchTail.min,navShaftTouchTail.max),opts.navdir));o_shadows&&setShadow($nav,findShadowEdge(newPos,navShaftTouchTail.min,navShaftTouchTail.max,opts.navdir));navWheelTail.prevent={"<":newPos>=navShaftTouchTail.max,">":newPos<=navShaftTouchTail.min};clearTimeout(navWheelTail.t);navWheelTail.t=setTimeout(function(){slideNavShaft.l=newPos;thumbsDraw(newPos,true)},TOUCH_TIMEOUT);thumbsDraw(newPos)}});$wrap.hover(function(){setTimeout(function(){if(touchedFLAG)return;toggleControlsClass(!(hoverFLAG=true))},0)},function(){if(!hoverFLAG)return;toggleControlsClass(!(hoverFLAG=false))});function onNavFrameClick(e){var index=$(this).data().eq;if(opts.navtype==="thumbs"){clickToShow({index:index,slow:e.altKey,user:true,coo:e._x-$nav.offset().left})}else{clickToShow({index:index,slow:e.altKey,user:true})}}function onArrClick(e){clickToShow({index:$arrs.index(this)?">":"<",slow:e.altKey,user:true})}smartClick($arrs,function(e){stopEvent(e);onArrClick.call(this,e)},{onStart:function(){onTouchStart();stageShaftTouchTail.control=true},onTouchEnd:onTouchEnd});smartClick($thumbArrLeft,function(e){stopEvent(e);if(opts.navtype==="thumbs"){that.show("<")}else{that.showSlide("prev")}});smartClick($thumbArrRight,function(e){stopEvent(e);if(opts.navtype==="thumbs"){that.show(">")}else{that.showSlide("next")}});function addFocusOnControls(el){addFocus(el,function(){setTimeout(function(){lockScroll($stage)},0);toggleControlsClass(false)})}$arrs.each(function(){addEnterUp(this,function(e){onArrClick.call(this,e)});addFocusOnControls(this)});addEnterUp(fullscreenIcon,function(){if($fotorama.hasClass(fullscreenClass)){that.cancelFullScreen();$stageShaft.focus()}else{that.requestFullScreen();$fullscreenIcon.focus()}});addFocusOnControls(fullscreenIcon);function reset(){setData();setOptions();if(!reset.i){reset.i=true;var _startindex=opts.startindex;activeIndex=repositionIndex=dirtyIndex=lastActiveIndex=startIndex=edgeIndex(_startindex)||0}if(size){if(changeToRtl())return;if($videoPlaying){unloadVideo($videoPlaying,true)}activeIndexes=[];detachFrames(STAGE_FRAME_KEY);reset.ok=true;that.show({index:activeIndex,time:0});that.resize()}else{that.destroy()}}function changeToRtl(){if(!changeToRtl.f===o_rtl){changeToRtl.f=o_rtl;activeIndex=size-1-activeIndex;that.reverse();return true}}$.each("load push pop shift unshift reverse sort splice".split(" "),function(i,method){that[method]=function(){data=data||[];if(method!=="load"){Array.prototype[method].apply(data,arguments)}else if(arguments[0]&&typeof arguments[0]==="object"&&arguments[0].length){data=clone(arguments[0])}reset();return that}});function ready(){if(ready.ok){ready.ok=false;triggerEvent("ready")}}reset()};$.fn.fotorama=function(opts){return this.each(function(){var that=this,$fotorama=$(this),fotoramaData=$fotorama.data(),fotorama=fotoramaData.fotorama;if(!fotorama){waitFor(function(){return!isHidden(that)},function(){fotoramaData.urtext=$fotorama.html();new $.Fotorama($fotorama,$.extend({},OPTIONS,window.fotoramaDefaults,opts,fotoramaData))})}else{fotorama.setOptions(opts,true)}})};$.Fotorama.instances=[];function calculateIndexes(){$.each($.Fotorama.instances,function(index,instance){instance.index=index})}function addInstance(instance){$.Fotorama.instances.push(instance);calculateIndexes()}function hideInstance(instance){$.Fotorama.instances.splice(instance.index,1);calculateIndexes()}$.Fotorama.cache={};$.Fotorama.measures={};$=$||{};$.Fotorama=$.Fotorama||{};$.Fotorama.jst=$.Fotorama.jst||{};$.Fotorama.jst.dots=function(v){var __t,__p="",__e=_.escape;__p+='
\r\n
\r\n
';return __p};$.Fotorama.jst.frameCaption=function(v){var __t,__p="",__e=_.escape;__p+='\r\n";return __p};$.Fotorama.jst.style=function(v){var __t,__p="",__e=_.escape;__p+=".fotorama"+((__t=v.s)==null?"":__t)+" .fotorama__nav--thumbs .fotorama__nav__frame{\r\npadding:"+((__t=v.m)==null?"":__t)+"px;\r\nheight:"+((__t=v.h)==null?"":__t)+"px}\r\n.fotorama"+((__t=v.s)==null?"":__t)+" .fotorama__thumb-border{\r\nheight:"+((__t=v.h)==null?"":__t)+"px;\r\nborder-width:"+((__t=v.b)==null?"":__t)+"px;\r\nmargin-top:"+((__t=v.m)==null?"":__t)+"px}";return __p};$.Fotorama.jst.thumb=function(v){var __t,__p="",__e=_.escape;__p+='
\r\n
\r\n
\r\n
';return __p}})(window,document,location,typeof jQuery!=="undefined"&&jQuery); diff --git a/lib/web/mage/adminhtml/globals.js b/lib/web/mage/adminhtml/globals.js index 12c97fdfcd2c5..683606e576497 100644 --- a/lib/web/mage/adminhtml/globals.js +++ b/lib/web/mage/adminhtml/globals.js @@ -12,7 +12,7 @@ define([ /** * Set of a temporary methods used to provide - * backward compatability with a legacy code. + * backward compatibility with a legacy code. */ window.setLocation = function (url) { window.location.href = url; diff --git a/lib/web/mage/adminhtml/grid.js b/lib/web/mage/adminhtml/grid.js index c9af869d79161..5f7a709f04fea 100644 --- a/lib/web/mage/adminhtml/grid.js +++ b/lib/web/mage/adminhtml/grid.js @@ -530,13 +530,20 @@ define([ /** * @param {Object} event + * @param {*} lastId */ - inputPage: function (event) { + inputPage: function (event, lastId) { var element = Event.element(event), - keyCode = event.keyCode || event.which; + keyCode = event.keyCode || event.which, + enteredValue = parseInt(element.value, 10), + pageId = parseInt(lastId, 10); if (keyCode == Event.KEY_RETURN) { //eslint-disable-line eqeqeq - this.setPage(element.value); + if (enteredValue > pageId) { + this.setPage(pageId); + } else { + this.setPage(enteredValue); + } } /*if(keyCode>47 && keyCode<58){ diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js index e6f12a2e51acf..96091e4099676 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentovariable/editor_plugin.js @@ -9,7 +9,8 @@ define([ 'Magento_Variable/js/config-directive-generator', 'Magento_Variable/js/custom-directive-generator', 'wysiwygAdapter', - 'jquery' + 'jquery', + 'mage/adminhtml/tools' ], function (configDirectiveGenerator, customDirectiveGenerator, wysiwyg, jQuery) { return function (config) { tinymce.create('tinymce.plugins.magentovariable', { diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js index 18d71aad2071a..cfcdef0b701c9 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js @@ -132,7 +132,7 @@ define([ attributes.type = attributes.type.replace(/\\\\/g, '\\'); imageSrc = config.placeholders[attributes.type]; - if (config.types.indexOf(attributes['type_name']) > -1) { + if (imageSrc) { imageHtml += ''; } else { @@ -147,8 +147,8 @@ define([ imageHtml += ' src="' + imageSrc + '"'; imageHtml += ' />'; - if (attributes['type_name']) { - imageHtml += attributes['type_name']; + if (config.types[attributes.type]) { + imageHtml += config.types[attributes.type]; } imageHtml += ''; diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js index 9374bac405c46..9779be85133f8 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js @@ -350,7 +350,7 @@ define([ * @param {String} content */ setContent: function (content) { - this.get(this.getId()).execCommand('mceSetContent', false, content); + this.get(this.getId()).setContent(content); }, /** @@ -559,10 +559,12 @@ define([ var selection = editor.selection, dom = editor.dom, rng = dom.createRng(), + doc = editor.getDoc(), markerHtml, marker; - if (!selection.getContent().length) { + // Validate the range we're trying to fix is contained within the current editors document + if (!selection.getContent().length && jQuery.contains(doc, selection.getRng().startContainer)) { markerHtml = '\uFEFF'; selection.setContent(markerHtml); marker = dom.get('mce_marker'); diff --git a/lib/web/mage/adminhtml/wysiwyg/widget.js b/lib/web/mage/adminhtml/wysiwyg/widget.js index 68206fdec6201..aa38e2e1875f6 100644 --- a/lib/web/mage/adminhtml/wysiwyg/widget.js +++ b/lib/web/mage/adminhtml/wysiwyg/widget.js @@ -456,7 +456,7 @@ define([ parameters: params, onComplete: function (transport) { try { - editor = wysiwyg.activeEditor(); + editor = wysiwyg.get(this.widgetTargetId); widgetTools.onAjaxSuccess(transport); widgetTools.dialogWindow.modal('closeModal'); @@ -469,7 +469,7 @@ define([ editor.selection.select(activeNode); editor.selection.setContent(transport.responseText); } else if (this.bMark) { - wysiwyg.activeEditor().selection.moveToBookmark(this.bMark); + editor.selection.moveToBookmark(this.bMark); } } @@ -513,7 +513,7 @@ define([ * @return {null|wysiwyg.Editor|*} */ getWysiwyg: function () { - return wysiwyg.activeEditor(); + return wysiwyg.get(this.widgetTargetId); }, /** diff --git a/lib/web/mage/backend/floating-header.js b/lib/web/mage/backend/floating-header.js index 06861277559a4..a6f767259488a 100644 --- a/lib/web/mage/backend/floating-header.js +++ b/lib/web/mage/backend/floating-header.js @@ -48,6 +48,7 @@ define([ this.element.wrapInner($('
', { 'class': 'page-actions-inner', 'data-title': title })); + this.element.removeClass('floating-header'); }, /** diff --git a/lib/web/mage/calendar.js b/lib/web/mage/calendar.js index ac154b333801d..a9ccf2cf787f9 100644 --- a/lib/web/mage/calendar.js +++ b/lib/web/mage/calendar.js @@ -66,6 +66,9 @@ * Widget calendar */ $.widget('mage.calendar', { + options: { + autoComplete: true + }, /** * Merge global options with options passed to widget invoke @@ -379,6 +382,9 @@ .addClass('v-middle') .text('') // Remove jQuery UI datepicker generated image .append('' + pickerButtonText + ''); + + $(element).attr('autocomplete', this.options.autoComplete ? 'on' : 'off'); + this._setCurrentDate(element); }, diff --git a/lib/web/mage/dataPost.js b/lib/web/mage/dataPost.js index 5d052f12db8fb..cc56ee266e08a 100644 --- a/lib/web/mage/dataPost.js +++ b/lib/web/mage/dataPost.js @@ -57,7 +57,7 @@ define([ */ postData: function (params) { var formKey = $(this.options.formKeyInputSelector).val(), - $form; + $form, input; if (formKey) { params.data['form_key'] = formKey; @@ -67,6 +67,19 @@ define([ data: params })); + if (params.files) { + $form[0].enctype = 'multipart/form-data'; + $.each(params.files, function (key, files) { + if (files instanceof FileList) { + input = document.createElement('input'); + input.type = 'file'; + input.name = key; + input.files = files; + $form[0].appendChild(input); + } + }); + } + if (params.data.confirmation) { uiConfirm({ content: params.data.confirmationMessage, diff --git a/lib/web/mage/gallery/gallery.js b/lib/web/mage/gallery/gallery.js index 15c3d01cf2be3..be78856b21fcd 100644 --- a/lib/web/mage/gallery/gallery.js +++ b/lib/web/mage/gallery/gallery.js @@ -141,7 +141,7 @@ define([ this.setupBreakpoints(); this.initFullscreenSettings(); - this.settings.$element.on('mouseup', '.fotorama__stage__frame', function () { + this.settings.$element.on('click', '.fotorama__stage__frame', function () { if ( !$(this).parents('.fotorama__shadows--left, .fotorama__shadows--right').length && !$(this).hasClass('fotorama-video-container') diff --git a/lib/web/mage/gallery/gallery.less b/lib/web/mage/gallery/gallery.less index 373708ac35a00..2c3476732caad 100644 --- a/lib/web/mage/gallery/gallery.less +++ b/lib/web/mage/gallery/gallery.less @@ -977,12 +977,9 @@ // While first time init .gallery-placeholder { - .loading-mask { - padding: 0 0 50%; - position: static; - } - .loader img { - position: absolute; + &__image { + display: block; + margin: auto; } } @@ -1003,6 +1000,7 @@ display: block; } } + .fotorama__product-video--loaded { .fotorama__img, .fotorama__img--full { display: none !important; diff --git a/lib/web/mage/tabs.js b/lib/web/mage/tabs.js index b441477ab8d8a..65c452d33bf12 100644 --- a/lib/web/mage/tabs.js +++ b/lib/web/mage/tabs.js @@ -72,7 +72,7 @@ define([ if (anchor && isValid) { $.each(self.contents, function (i) { - if ($(this).attr('id') === anchorId) { + if ($(this).attr('id') === anchorId || $(this).find('#' + anchorId).length) { self.collapsibles.not(self.collapsibles.eq(i)).collapsible('forceDeactivate'); return false; diff --git a/lib/web/mage/translate-init.js b/lib/web/mage/translate-init.js deleted file mode 100644 index 1b9defad5e397..0000000000000 --- a/lib/web/mage/translate-init.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'mage/translate', - 'jquery/jquery-storageapi' -], function ($) { - 'use strict'; - - return function (pageOptions) { - var dependencies = [], - versionObj; - - $.initNamespaceStorage('mage-translation-storage'); - $.initNamespaceStorage('mage-translation-file-version'); - versionObj = $.localStorage.get('mage-translation-file-version'); - - if (versionObj.version !== pageOptions.version) { - dependencies.push( - pageOptions.dictionaryFile - ); - } - - require.config({ - deps: dependencies, - - /** - * @param {String} string - */ - callback: function (string) { - if (typeof string === 'string') { - $.mage.translate.add(JSON.parse(string)); - $.localStorage.set('mage-translation-storage', string); - $.localStorage.set( - 'mage-translation-file-version', - { - version: pageOptions.version - } - ); - } else { - $.mage.translate.add($.localStorage.get('mage-translation-storage')); - } - } - }); - }; -}); diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 01ecaf7cf46c2..73c5ef4d4f02f 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1431,10 +1431,14 @@ ], 'validate-per-page-value-list': [ function (v) { - var isValid = !$.mage.isEmpty(v), + var isValid = true, values = v.split(','), i; + if ($.mage.isEmpty(v)) { + return isValid; + } + for (i = 0; i < values.length; i++) { if (!/^[0-9]+$/.test(values[i])) { isValid = false; @@ -1777,7 +1781,8 @@ valid = true, validateConfig = { errorElement: 'label', - ignore: '.ignore-validate' + ignore: '.ignore-validate', + hideError: false }, form, validator, classes, elementValue; @@ -1815,7 +1820,10 @@ valid = false; errors[element.get(0).name] = this.messages[className]; validator.invalid[element.get(0).name] = true; - validator.showErrors(errors); + + if (!validateConfig.hideError) { + validator.showErrors(errors); + } return valid; } @@ -1950,7 +1958,7 @@ } if (firstActive.length) { - $('html, body').animate({ + $('html, body').stop().animate({ scrollTop: firstActive.offset().top }); firstActive.focus(); diff --git a/nginx.conf.sample b/nginx.conf.sample index 90604808f6ec0..979ac0be1f537 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -9,6 +9,7 @@ # listen 80; # server_name mage.dev; # set $MAGE_ROOT /var/www/magento2; +# set $MAGE_DEBUG_SHOW_ARGS 1; # include /vagrant/magento2/nginx.conf.sample; # } # @@ -33,6 +34,12 @@ charset UTF-8; error_page 404 403 = /errors/404.php; #add_header "X-UA-Compatible" "IE=Edge"; + +# Deny access to sensitive files +location /.user.ini { + deny all; +} + # PHP entry point for setup application location ~* ^/setup($|/) { root $MAGE_ROOT; @@ -99,7 +106,7 @@ location /static/ { # Remove signature of the static files that is used to overcome the browser cache location ~ ^/static/version { - rewrite ^/static/(version[^/]+/)?(.*)$ /static/$2 last; + rewrite ^/static/(version\d*/)?(.*)$ /static/$2 last; } location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2|json)$ { @@ -108,7 +115,7 @@ location /static/ { expires +1y; if (!-f $request_filename) { - rewrite ^/static/?(.*)$ /static.php?resource=$1 last; + rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { @@ -117,11 +124,11 @@ location /static/ { expires off; if (!-f $request_filename) { - rewrite ^/static/?(.*)$ /static.php?resource=$1 last; + rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } if (!-f $request_filename) { - rewrite ^/static/?(.*)$ /static.php?resource=$1 last; + rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } add_header X-Frame-Options "SAMEORIGIN"; } @@ -159,6 +166,11 @@ location /media/downloadable/ { location /media/import/ { deny all; } +location /errors/ { + location ~* \.xml$ { + deny all; + } +} # PHP entry point for main application location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { @@ -198,6 +210,6 @@ gzip_types gzip_vary on; # Banned locations (only reached if the earlier PHP entry point regexes don't match) -location ~* (\.php$|\.htaccess$|\.git) { +location ~* (\.php$|\.phtml$|\.htaccess$|\.git) { deny all; } diff --git a/phpserver/README.md b/phpserver/README.md index 414ad77ae6b33..563d2ed7c9fc9 100644 --- a/phpserver/README.md +++ b/phpserver/README.md @@ -31,7 +31,7 @@ For more informations about the installation process using the CLI, you can cons ### How to run Magento -Example usage: ```php -S 127.0.0.1:8082 -t ./pub/ ../phpserver/router.php``` +Example usage: ```php -S 127.0.0.1:8082 -t ./pub/ ./phpserver/router.php``` ### What exactly the script does diff --git a/pub/.htaccess b/pub/.htaccess index 8ba04ff4415f3..6a97a6d14dc00 100644 --- a/pub/.htaccess +++ b/pub/.htaccess @@ -47,11 +47,6 @@ php_flag session.auto_start off ############################################ -## Enable resulting html compression - - #php_flag zlib.output_compression on - -########################################### # Disable user agent verification to not break multiple image upload php_flag suhosin.session.cryptua off @@ -220,6 +215,16 @@ ErrorDocument 403 /errors/404.php Require all denied +## Deny access to .user.ini + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + ############################################ diff --git a/pub/errors/.htaccess b/pub/errors/.htaccess index 3692dd439e2ff..a7b9cbda05893 100644 --- a/pub/errors/.htaccess +++ b/pub/errors/.htaccess @@ -1,4 +1,7 @@ Options None + + Deny from all + RewriteEngine Off diff --git a/pub/errors/processor.php b/pub/errors/processor.php index cff3a14921d38..ab21f791bc021 100644 --- a/pub/errors/processor.php +++ b/pub/errors/processor.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Error; use Magento\Framework\Serialize\Serializer\Json; @@ -11,6 +13,7 @@ * Error processor * * @SuppressWarnings(PHPMD.TooManyFields) + * phpcs:ignoreFile */ class Processor { @@ -501,7 +504,6 @@ public function saveReport($reportData) * * @param int $reportId * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ public function loadReport($reportId) { diff --git a/pub/health_check.php b/pub/health_check.php index c9a4965876bb7..fc6d73daa2079 100644 --- a/pub/health_check.php +++ b/pub/health_check.php @@ -4,11 +4,17 @@ * See COPYING.txt for license details. */ +/** + * phpcs:disable PSR1.Files.SideEffects + * phpcs:disable Squiz.Functions.GlobalFunction + */ use Magento\Framework\Config\ConfigOptionsListConstants; +// phpcs:ignore Magento2.Functions.DiscouragedFunction register_shutdown_function("fatalErrorHandler"); try { + // phpcs:ignore Magento2.Security.IncludeFile require __DIR__ . '/../app/bootstrap.php'; /** @var \Magento\Framework\App\ObjectManagerFactory $objectManagerFactory */ $objectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, []); @@ -20,6 +26,7 @@ $logger = $objectManager->get(\Psr\Log\LoggerInterface::class); } catch (\Exception $e) { http_response_code(500); + // phpcs:ignore Magento2.Security.LanguageConstruct exit(1); } @@ -35,6 +42,7 @@ } catch (\Exception $e) { http_response_code(500); $logger->error("MySQL connection failed: " . $e->getMessage()); + // phpcs:ignore Magento2.Security.LanguageConstruct exit(1); } } @@ -47,6 +55,7 @@ !isset($cacheConfig[ConfigOptionsListConstants::CONFIG_PATH_BACKEND_OPTIONS])) { http_response_code(500); $logger->error("Cache configuration is invalid"); + // phpcs:ignore Magento2.Security.LanguageConstruct exit(1); } $cacheBackendClass = $cacheConfig[ConfigOptionsListConstants::CONFIG_PATH_BACKEND]; @@ -57,6 +66,7 @@ } catch (\Exception $e) { http_response_code(500); $logger->error("Cache storage is not accessible"); + // phpcs:ignore Magento2.Security.LanguageConstruct exit(1); } } @@ -70,7 +80,7 @@ function fatalErrorHandler() { $error = error_get_last(); - if ($error !== null) { + if ($error !== null && $error['type'] === E_ERROR) { http_response_code(500); } } diff --git a/pub/media/.htaccess b/pub/media/.htaccess index 28e65b490fbb8..d8793a891430a 100644 --- a/pub/media/.htaccess +++ b/pub/media/.htaccess @@ -23,6 +23,9 @@ SetHandler default-handler Options +FollowSymLinks RewriteEngine on + ## you can put here your pub/media folder path relative to web root + #RewriteBase /magento/pub/media/ + ############################################ ## never rewrite for existing files RewriteCond %{REQUEST_FILENAME} !-f diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index e7e3087419b55..5521f7024722b 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -34,6 +34,11 @@ ${__P(request_protocol,http)} = + + graphql_port_number + ${__P(graphql_port_number,)} + = + admin_password ${__P(admin_password,123123q)} @@ -354,6 +359,136 @@ ${__P(frontendPoolUsers,1)} = + + graphQLPoolUsers + ${__P(graphQLPoolUsers,1)} + = + + + graphqlAddConfigurableProductToCartPercentage + ${__P(graphqlAddConfigurableProductToCartPercentage,0)} + = + + + graphqlAddSimpleProductToCartPercentage + ${__P(graphqlAddSimpleProductToCartPercentage,0)} + = + + + graphqlApplyCouponToCartPercentage + ${__P(graphqlApplyCouponToCartPercentage,0)} + = + + + graphqlCatalogBrowsingByGuestPercentage + ${__P(graphqlCatalogBrowsingByGuestPercentage,0)} + = + + + graphqlCheckoutByGuestPercentage + ${__P(graphqlCheckoutByGuestPercentage,0)} + = + + + graphqlCreateEmptyCartPercentage + ${__P(graphqlCreateEmptyCartPercentage,0)} + = + + + graphqlGetCategoryListByCategoryIdPercentage + ${__P(graphqlGetCategoryListByCategoryIdPercentage,0)} + = + + + graphqlGetCmsBlockByIdentifierPercentage + ${__P(graphqlGetCmsBlockByIdentifierPercentage,0)} + = + + + graphqlGetCmsPageByIdPercentage + ${__P(graphqlGetCmsPageByIdPercentage,0)} + = + + + graphqlGetConfigurableProductDetailsByNamePercentage + ${__P(graphqlGetConfigurableProductDetailsByNamePercentage,0)} + = + + + graphqlGetConfigurableProductDetailsByProductUrlKeyPercentage + ${__P(graphqlGetConfigurableProductDetailsByProductUrlKeyPercentage,0)} + = + + + graphqlGetEmptyCartPercentage + ${__P(graphqlGetEmptyCartPercentage,0)} + = + + + graphqlGetListOfProductsByCategoryIdPercentage + ${__P(graphqlGetListOfProductsByCategoryIdPercentage,0)} + = + + + graphqlGetNavigationMenuByCategoryIdPercentage + ${__P(graphqlGetNavigationMenuByCategoryIdPercentage,0)} + = + + + graphqlGetProductSearchByTextAndCategoryIdPercentage + ${__P(graphqlGetProductSearchByTextAndCategoryIdPercentage,0)} + = + + + graphqlGetSimpleProductDetailsByNamePercentage + ${__P(graphqlGetSimpleProductDetailsByNamePercentage,0)} + = + + + graphqlGetSimpleProductDetailsByProductUrlKeyPercentage + ${__P(graphqlGetSimpleProductDetailsByProductUrlKeyPercentage,0)} + = + + + graphqlRemoveConfigurableProductFromCartPercentage + ${__P(graphqlRemoveConfigurableProductFromCartPercentage,0)} + = + + + graphqlRemoveCouponFromCartPercentage + ${__P(graphqlRemoveCouponFromCartPercentage,0)} + = + + + graphqlRemoveSimpleProductFromCartPercentage + ${__P(graphqlRemoveSimpleProductFromCartPercentage,0)} + = + + + graphqlSetBillingAddressOnCartPercentage + ${__P(graphqlSetBillingAddressOnCartPercentage,0)} + = + + + graphqlSetShippingAddressOnCartPercentage + ${__P(graphqlSetShippingAddressOnCartPercentage,0)} + = + + + graphqlUpdateConfigurableProductQtyInCartPercentage + ${__P(graphqlUpdateConfigurableProductQtyInCartPercentage,0)} + = + + + graphqlUpdateSimpleProductQtyInCartPercentage + ${__P(graphqlUpdateSimpleProductQtyInCartPercentage,0)} + = + + + graphqlUrlInfoByUrlKeyPercentage + ${__P(graphqlUrlInfoByUrlKeyPercentage,0)} + = + guest_checkout_percent ${__P(guest_checkout_percent,100)} @@ -658,6 +793,10 @@ props.remove("configurable_products_list"); props.remove("configurable_products_list_for_edit"); props.remove("users"); props.remove("customer_emails_list"); +props.remove("categories"); +props.remove("cms_pages"); +props.remove("cms_blocks"); +props.remove("coupon_codes"); /* This is only used when admin is enabled. */ props.put("activeAdminThread", ""); @@ -821,167 +960,253 @@ if (!slash.equals(path.substring(path.length() -1)) || !slash.equals(path.substr - - mpaf/tool/fragments/ce/setup/extract_categories.jmx + + mpaf/tool/fragments/ce/setup/extract_admin_users.jmx - - - - - - Content-Type - application/json - - - Accept - */* + + + + + false + ${admin_form_key} + = + true + form_key - + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/user/roleGrid/limit/200/?ajax=true&isAjax=true + POST + true + false + true + false + false + + + + + + + false + import java.util.regex.Pattern; + import java.util.regex.Matcher; + import java.util.LinkedList; + + LinkedList adminUserList = new LinkedList(); + String response = new String(data); + Pattern pattern = Pattern.compile("<td\\W*?data-column=.username[^>]*?>\\W*?(\\w+)\\W*?<"); + Matcher matcher = pattern.matcher(response); + + while (matcher.find()) { + adminUserList.add(matcher.group(1)); + } + + adminUserList.poll(); + props.put("adminUserList", adminUserList); + props.put("adminUserListIterator", adminUserList.descendingIterator()); + + - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/integration/admin/token - POST - true - false - true - false - false - - - - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - Authorization - Bearer ${admin_token} + + + + + mpaf/tool/fragments/ce/setup/extract_customers.jmx + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/customer/index/ + GET + true + false + true + false + false + + + + + + + true + import org.apache.jmeter.protocol.http.control.CookieManager; +import org.apache.jmeter.protocol.http.control.Cookie; +CookieManager manager = sampler.getCookieManager(); +Cookie cookie = new Cookie("adminhtml",vars.get("COOKIE_adminhtml"),vars.get("host"),"/",false,0); +manager.add(cookie); + + + + + Customers + <title>Customers / Customers / Magento Admin</title> + + Assertion.response_data + false + 2 + + + + + + + + true + customer_listing + = + true + namespace + + + true + entity_id + = + true + sorting[field] + + + true + asc + = + true + sorting[direction] + + + true + true + = + true + isAjax + + + true + customer_since[locale]=en_US + = + true + filters[placeholder] + + + true + 1 + = + true + filters[group_id] + + + true + 1 + = + true + filters[website_id] + + + true + ${customers_page_size} + = + true + paging[pageSize] + + + true + 1 + = + true + paging[current] + + + true + entity_id + = + true + sorting[field] + + + true + asc + = + true + sorting[direction] + + + true + true + = + true + isAjax - + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/mui/index/render/ + GET + true + false + true + false + false + + + + + $.totalRecords + 0 + true + false + true + true + + + + customer_emails + $.items[*].email + + + BODY + + + + customer_ids + $.items[*].entity_id + + + BODY + + + + false + + + import java.util.LinkedList; +LinkedList emailsList = new LinkedList(); +props.put("customer_emails_list", emailsList); + - - - - - true - path - = - true - searchCriteria[filterGroups][0][filters][0][field] - - - true - 1/2/% - = - true - searchCriteria[filterGroups][0][filters][0][value] - - - true - like - = - true - searchCriteria[filterGroups][0][filters][0][conditionType] - - - true - level - = - true - searchCriteria[filterGroups][1][filters][0][field] - - - true - 2 - = - true - searchCriteria[filterGroups][1][filters][0][value] - - - true - ${categories_count} - = - true - searchCriteria[pageSize] - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/categories/list - GET - true - false - false - false - false - - - - - false - category_url_keys - url_key\",\"value\":\"(.*?)\" - $1$ - - -1 - - - - false - category_names - name\":\"(.*?)\" - $1$ - - -1 - - - - - category_url_keys - category_url_key + + customer_emails + customer_email true @@ -989,32 +1214,31 @@ if (!slash.equals(path.substring(path.length() -1)) || !slash.equals(path.substr 1 1 - category_url_key_counter + email_counter false - - import java.util.ArrayList; + + +try { -// If it is first iteration of cycle then recreate category url key list -if (1 == Integer.parseInt(vars.get("category_url_key_counter"))) { - categoryUrlKeysList = new ArrayList(); - props.put("category_url_keys_list", categoryUrlKeysList); - props.put("category_url_key", vars.get("category_url_key")); -} else { - categoryUrlKeysList = props.get("category_url_keys_list"); +props.get("customer_emails_list").add(vars.get("customer_email")); + +} catch (java.lang.Exception e) { + log.error("error…", e); + SampleResult.setStopThread(true); } -categoryUrlKeysList.add(vars.get("category_url_key")); + false - - category_names - category_name + + customer_ids + customer_id true @@ -1022,166 +1246,177 @@ categoryUrlKeysList.add(vars.get("category_url_key")); 1 1 - category_name_counter + id_counter false - + import java.util.ArrayList; -// If it is first iteration of cycle then recreate category name list -if (1 == Integer.parseInt(vars.get("category_name_counter"))) { - categoryNamesList = new ArrayList(); - props.put("category_names_list",categoryNamesList); - props.put("category_name", vars.get("category_name")); +// If it is first iteration of cycle then recreate idsList +if (1 == Integer.parseInt(vars.get("id_counter"))) { + idsList = new ArrayList(); + props.put("customer_ids_list", idsList); } else { - categoryNamesList = props.get("category_names_list"); + idsList = props.get("customer_ids_list"); } - -categoryNamesList.add(vars.get("category_name")); +idsList.add(vars.get("customer_id")); false - - props.put("category_url_key", vars.get("category_url_key")); -props.put("category_name", vars.get("category_name")); - - - false - - - - mpaf/tool/fragments/ce/setup/extract_categories_id_of_last_level.jmx - + + mpaf/tool/fragments/ce/setup/extract_region_ids.jmx + - - props.remove("admin_category_ids_list"); - - - false - - - - - - - - Content-Type - application/json - - - Accept - */* + + + + + false + US + = + true + parent - - - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/integration/admin/token - POST - true - false - true - false - false - - - - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - Authorization - Bearer ${admin_token} - - - + + + + + + ${request_protocol} + + ${base_path}${admin_path}/directory/json/countryRegion/ + GET + true + false + true + false + false + + + + + groovy + + + + import groovy.json.JsonSlurper +def jsonSlurper = new JsonSlurper(); +def regionResponse = jsonSlurper.parseText(prev.getResponseDataAsString()); + +regionResponse.each { region -> + if (region.label.toString() == "Alabama") { + props.put("alabama_region_id", region.value.toString()); + } else if (region.label.toString() == 'California') { + props.put("california_region_id", region.value.toString()); + } +} + - + + + + + mpaf/tool/fragments/ce/simple_controller.jmx + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + true + + + + false + {"username":"${admin_user}","password":"${admin_password}"} + = + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}rest/V1/integration/admin/token + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/api/admin_token_retrieval.jmx + + + admin_token + $ + + + BODY + + + + + ^[a-z0-9-]+$ + + Assertion.response_data + false + 1 + variable + admin_token + + + + + + + + Authorization + Bearer ${admin_token} + + + mpaf/tool/fragments/ce/api/header_manager.jmx + + + + + - - true - children_count - = - true - searchCriteria[filterGroups][0][filters][0][field] - - - true - 0 - = - true - searchCriteria[filterGroups][0][filters][0][value] - - - true - level - = - true - searchCriteria[filterGroups][1][filters][0][field] - - + true - 2 - = - true - searchCriteria[filterGroups][1][filters][0][value] - - - true - gt + 1 = true - searchCriteria[filterGroups][1][filters][0][conditionType] + searchCriteria[current_page] - - true - ${adminCategoryCount} + + false + 20 = true - searchCriteria[pageSize] + searchCriteria[page_size] @@ -1191,7 +1426,7 @@ props.put("category_name", vars.get("category_name")); 200000 ${request_protocol} - ${base_path}rest/default/V1/categories/list + ${base_path}rest/default/V1/cmsPage/search GET true false @@ -1199,118 +1434,42 @@ props.put("category_name", vars.get("category_name")); false false + mpaf/tool/fragments/ce/setup/get_cms_pages.jmx - - false - category_list_id - \{\"id\":(\d+), - $1$ - - -1 - + + $.total_count + 0 + true + false + true + false + - - - category_list_id - category_id - true - - - - import java.util.ArrayList; + + javascript + + + + var data = JSON.parse(prev.getResponseDataAsString()); -adminCategoryIdsList = props.get("admin_category_ids_list"); -// If it is first iteration of cycle then recreate categories ids list -if (adminCategoryIdsList == null) { - adminCategoryIdsList = new ArrayList(); - props.put("admin_category_ids_list", adminCategoryIdsList); -} -adminCategoryIdsList.add(vars.get("category_id")); - - - false - +var cmsPages = []; + +for (var i in data.items) { + cmsPages.push({"id": data.items[i].id, "identifier": data.items[i].identifier}); + } + +props.put("cms_pages", cmsPages); + + - mpaf/tool/fragments/ce/setup/extract_configurable_products.jmx - - - - - - Content-Type - application/json - - - Accept - */* - - - - - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/integration/admin/token - POST - true - false - true - false - false - - - - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - Authorization - Bearer ${admin_token} - - - - @@ -1437,83 +1596,11 @@ productList.add(productMap); - mpaf/tool/fragments/ce/setup/extract_configurable_products_for_edit.jmx - - - - - - Content-Type - application/json - - - Accept - */* - - - - - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/integration/admin/token - POST - true - false - true - false - false - - - - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - Authorization - Bearer ${admin_token} - - - - @@ -1645,85 +1732,13 @@ editProductList.add(editProductMap); false - - - mpaf/tool/fragments/ce/setup/extract_simple_products_for_edit.jmx + + mpaf/tool/fragments/ce/setup/extract_simple_products.jmx - - - - - - Content-Type - application/json - - - Accept - */* - - - - - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/integration/admin/token - POST - true - false - true - false - false - - - - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - Authorization - Bearer ${admin_token} - - - - - + @@ -1747,26 +1762,19 @@ editProductList.add(editProductMap); true searchCriteria[pageSize] - - true - 1 - = - true - searchCriteria[currentPage] - - + true attribute_set_id - = + != true - searchCriteria[filterGroups][1][filters][1][field] + searchCriteria[filterGroups][0][filters][1][field] - + true 4 = true - searchCriteria[filterGroups][1][filters][1][value] + searchCriteria[filterGroups][0][filters][1][value] @@ -1788,7 +1796,7 @@ editProductList.add(editProductMap); false - simple_products_for_edit_url_keys + simple_products_url_keys url_key\",\"value\":\"(.*?)\" $1$ @@ -1797,7 +1805,7 @@ editProductList.add(editProductMap); false - simple_product_for_edit_ids + simple_product_ids \"id\":(\d+),\"sku\" $1$ @@ -1806,7 +1814,7 @@ editProductList.add(editProductMap); false - simple_product_for_edit_names + simple_product_names name\":\"(.*?)\" $1$ @@ -1815,7 +1823,7 @@ editProductList.add(editProductMap); false - simple_product_for_edit_skus + simple_product_skus sku\":\"(.*?)\" $1$ @@ -1824,129 +1832,57 @@ editProductList.add(editProductMap); - - simple_product_for_edit_ids - simple_product_for_edit_id + + simple_product_ids + simple_product_id true - + 1 1 - simple_products_counter_for_edit + simple_products_counter false - + import java.util.ArrayList; import java.util.HashMap; import org.apache.commons.codec.binary.Base64; -if (1 == Integer.parseInt(vars.get("simple_products_counter_for_edit"))) { - editProductList = new ArrayList(); - props.put("simple_products_list_for_edit", editProductList); +// If it is first iteration of cycle then recreate productList +if (1 == Integer.parseInt(vars.get("simple_products_counter"))) { + productList = new ArrayList(); + props.put("simple_products_list", productList); } else { - productList = props.get("simple_products_counter_for_edit"); + productList = props.get("simple_products_list"); } - -String productUrl = vars.get("request_protocol") + "://" + vars.get("host") + vars.get("base_path") + vars.get("simple_products_for_edit_url_keys_" + vars.get("simple_products_counter_for_edit"))+ vars.get("url_suffix"); +String productUrl = vars.get("request_protocol") + "://" + vars.get("host") + vars.get("base_path") + vars.get("simple_products_url_keys_" + vars.get("simple_products_counter"))+ vars.get("url_suffix"); encodedUrl = Base64.encodeBase64(productUrl.getBytes()); // Create product map -Map editProductMap = new HashMap(); -editProductMap.put("id", vars.get("simple_product_for_edit_id")); -editProductMap.put("title", vars.get("simple_product_for_edit_names_" + vars.get("simple_products_counter_for_edit"))); -editProductMap.put("sku", vars.get("simple_product_for_edit_skus_" + vars.get("simple_products_counter_for_edit"))); -editProductMap.put("url_key", vars.get("simple_products_for_edit_url_keys_" + vars.get("simple_products_counter_for_edit"))); -editProductMap.put("uenc", new String(encodedUrl)); +Map productMap = new HashMap(); +productMap.put("id", vars.get("simple_product_id")); +productMap.put("title", vars.get("simple_product_names_" + vars.get("simple_products_counter"))); +productMap.put("sku", vars.get("simple_product_skus_" + vars.get("simple_products_counter"))); +productMap.put("url_key", vars.get("simple_products_url_keys_" + vars.get("simple_products_counter"))); +productMap.put("uenc", new String(encodedUrl)); // Collect products map in products list -editProductList.add(editProductMap); +productList.add(productMap); false - - - mpaf/tool/fragments/ce/setup/extract_simple_products.jmx + + mpaf/tool/fragments/ce/setup/extract_simple_products_for_edit.jmx - - - - - - Content-Type - application/json - - - Accept - */* - - - - - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/V1/integration/admin/token - POST - true - false - true - false - false - - - - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - Authorization - Bearer ${admin_token} - - - - - + @@ -1970,19 +1906,26 @@ editProductList.add(editProductMap); true searchCriteria[pageSize] - + + true + 1 + = + true + searchCriteria[currentPage] + + true attribute_set_id - != + = true - searchCriteria[filterGroups][0][filters][1][field] + searchCriteria[filterGroups][1][filters][1][field] - + true 4 = true - searchCriteria[filterGroups][0][filters][1][value] + searchCriteria[filterGroups][1][filters][1][value] @@ -2004,7 +1947,7 @@ editProductList.add(editProductMap); false - simple_products_url_keys + simple_products_for_edit_url_keys url_key\",\"value\":\"(.*?)\" $1$ @@ -2013,7 +1956,7 @@ editProductList.add(editProductMap); false - simple_product_ids + simple_product_for_edit_ids \"id\":(\d+),\"sku\" $1$ @@ -2022,7 +1965,7 @@ editProductList.add(editProductMap); false - simple_product_names + simple_product_for_edit_names name\":\"(.*?)\" $1$ @@ -2031,7 +1974,7 @@ editProductList.add(editProductMap); false - simple_product_skus + simple_product_for_edit_skus sku\":\"(.*?)\" $1$ @@ -2040,243 +1983,100 @@ editProductList.add(editProductMap); - - simple_product_ids - simple_product_id + + simple_product_for_edit_ids + simple_product_for_edit_id true - + 1 1 - simple_products_counter + simple_products_counter_for_edit false - + import java.util.ArrayList; import java.util.HashMap; import org.apache.commons.codec.binary.Base64; -// If it is first iteration of cycle then recreate productList -if (1 == Integer.parseInt(vars.get("simple_products_counter"))) { - productList = new ArrayList(); - props.put("simple_products_list", productList); +if (1 == Integer.parseInt(vars.get("simple_products_counter_for_edit"))) { + editProductList = new ArrayList(); + props.put("simple_products_list_for_edit", editProductList); } else { - productList = props.get("simple_products_list"); + productList = props.get("simple_products_counter_for_edit"); } -String productUrl = vars.get("request_protocol") + "://" + vars.get("host") + vars.get("base_path") + vars.get("simple_products_url_keys_" + vars.get("simple_products_counter"))+ vars.get("url_suffix"); + +String productUrl = vars.get("request_protocol") + "://" + vars.get("host") + vars.get("base_path") + vars.get("simple_products_for_edit_url_keys_" + vars.get("simple_products_counter_for_edit"))+ vars.get("url_suffix"); encodedUrl = Base64.encodeBase64(productUrl.getBytes()); // Create product map -Map productMap = new HashMap(); -productMap.put("id", vars.get("simple_product_id")); -productMap.put("title", vars.get("simple_product_names_" + vars.get("simple_products_counter"))); -productMap.put("sku", vars.get("simple_product_skus_" + vars.get("simple_products_counter"))); -productMap.put("url_key", vars.get("simple_products_url_keys_" + vars.get("simple_products_counter"))); -productMap.put("uenc", new String(encodedUrl)); +Map editProductMap = new HashMap(); +editProductMap.put("id", vars.get("simple_product_for_edit_id")); +editProductMap.put("title", vars.get("simple_product_for_edit_names_" + vars.get("simple_products_counter_for_edit"))); +editProductMap.put("sku", vars.get("simple_product_for_edit_skus_" + vars.get("simple_products_counter_for_edit"))); +editProductMap.put("url_key", vars.get("simple_products_for_edit_url_keys_" + vars.get("simple_products_counter_for_edit"))); +editProductMap.put("uenc", new String(encodedUrl)); // Collect products map in products list -productList.add(productMap); +editProductList.add(editProductMap); false - - - - mpaf/tool/fragments/ce/setup/extract_admin_users.jmx - - - - - - - false - ${admin_form_key} - = - true - form_key - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/user/roleGrid/limit/200/?ajax=true&isAjax=true - POST - true - false - true - false - false - - - - - - - false - import java.util.regex.Pattern; - import java.util.regex.Matcher; - import java.util.LinkedList; - - LinkedList adminUserList = new LinkedList(); - String response = new String(data); - Pattern pattern = Pattern.compile("<td\\W*?data-column=.username[^>]*?>\\W*?(\\w+)\\W*?<"); - Matcher matcher = pattern.matcher(response); - - while (matcher.find()) { - adminUserList.add(matcher.group(1)); - } - - adminUserList.poll(); - props.put("adminUserList", adminUserList); - props.put("adminUserListIterator", adminUserList.descendingIterator()); - - - - - - - mpaf/tool/fragments/ce/setup/extract_customers.jmx + + mpaf/tool/fragments/ce/setup/extract_categories.jmx - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/customer/index/ - GET - true - false - true - false - false - - - - - - - true - import org.apache.jmeter.protocol.http.control.CookieManager; -import org.apache.jmeter.protocol.http.control.Cookie; -CookieManager manager = sampler.getCookieManager(); -Cookie cookie = new Cookie("adminhtml",vars.get("COOKIE_adminhtml"),vars.get("host"),"/",false,0); -manager.add(cookie); - - - - - Customers - <title>Customers / Customers / Magento Admin</title> - - Assertion.response_data - false - 2 - - - - - + + - - true - customer_listing - = - true - namespace - - - true - entity_id - = - true - sorting[field] - - - true - asc - = - true - sorting[direction] - - - true - true - = - true - isAjax - - - true - customer_since[locale]=en_US - = - true - filters[placeholder] - - - true - 1 - = - true - filters[group_id] - - + true - 1 + path = true - filters[website_id] + searchCriteria[filterGroups][0][filters][0][field] - + true - ${customers_page_size} + 1/2/% = true - paging[pageSize] + searchCriteria[filterGroups][0][filters][0][value] - + true - 1 + like = true - paging[current] + searchCriteria[filterGroups][0][filters][0][conditionType] - + true - entity_id + level = true - sorting[field] + searchCriteria[filterGroups][1][filters][0][field] - + true - asc + 2 = true - sorting[direction] + searchCriteria[filterGroups][1][filters][0][value] - + true - true + ${categories_count} = true - isAjax + searchCriteria[pageSize] @@ -2286,140 +2086,180 @@ manager.add(cookie); 200000 ${request_protocol} - ${base_path}${admin_path}/mui/index/render/ + ${base_path}rest/V1/categories/list GET true false - true + false false false - - $.totalRecords - 0 - true - false - true - true - - - - customer_emails - $.items[*].email - - - BODY - - - - customer_ids - $.items[*].entity_id - - - BODY - - - - false + + javascript - import java.util.LinkedList; -LinkedList emailsList = new LinkedList(); -props.put("customer_emails_list", emailsList); - - - - - customer_emails - customer_email - true - - - - 1 - - 1 - email_counter - - false - - - - -try { - -props.get("customer_emails_list").add(vars.get("customer_email")); - -} catch (java.lang.Exception e) { - log.error("error…", e); - SampleResult.setStopThread(true); + + var data = JSON.parse(prev.getResponseDataAsString()); + +var categoryData = [], categoryNames = [], categoryUrls = []; + +for (var i in data.items) { + var cat = data.items[i], urlKey = getUrlKey(cat); + categoryData.push({"id": cat.id, "name": cat.name, "url_key": urlKey, "children": cat.children.split(",")}); + categoryNames.push(cat.name); + categoryUrls.push(urlKey); + } + +function getUrlKey(cat) { + for (var i in cat.custom_attributes) { + if (cat.custom_attributes[i].attribute_code == "url_key") { + return cat.custom_attributes[i].value; + } + } + return ""; } - - - - false - + +props.put("categories", categoryData); +props.put("category_url_keys_list", categoryUrls); +props.put("category_names_list",categoryNames); + - - customer_ids - customer_id - true - - - - 1 - - 1 - id_counter - - false - - - - import java.util.ArrayList; + + + + mpaf/tool/fragments/ce/setup/extract_categories_id_of_last_level.jmx + + + + props.remove("admin_category_ids_list"); + + + false + + + + + + + true + children_count + = + true + searchCriteria[filterGroups][0][filters][0][field] + + + true + 0 + = + true + searchCriteria[filterGroups][0][filters][0][value] + + + true + level + = + true + searchCriteria[filterGroups][1][filters][0][field] + + + true + 2 + = + true + searchCriteria[filterGroups][1][filters][0][value] + + + true + gt + = + true + searchCriteria[filterGroups][1][filters][0][conditionType] + + + true + ${adminCategoryCount} + = + true + searchCriteria[pageSize] + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}rest/default/V1/categories/list + GET + true + false + true + false + false + + + + + false + category_list_id + \{\"id\":(\d+), + $1$ + + -1 + + + + + category_list_id + category_id + true + + + + import java.util.ArrayList; -// If it is first iteration of cycle then recreate idsList -if (1 == Integer.parseInt(vars.get("id_counter"))) { - idsList = new ArrayList(); - props.put("customer_ids_list", idsList); -} else { - idsList = props.get("customer_ids_list"); +adminCategoryIdsList = props.get("admin_category_ids_list"); +// If it is first iteration of cycle then recreate categories ids list +if (adminCategoryIdsList == null) { + adminCategoryIdsList = new ArrayList(); + props.put("admin_category_ids_list", adminCategoryIdsList); } -idsList.add(vars.get("customer_id")); - - - false - - - +adminCategoryIdsList.add(vars.get("category_id")); + + + false + + + - - mpaf/tool/fragments/ce/setup/extract_region_ids.jmx + + mpaf/tool/fragments/ce/setup/extract_coupon_codes.jmx - + - + false - US + 10 = true - parent + searchCriteria[pageSize] - - + 60000 + 200000 ${request_protocol} - ${base_path}${admin_path}/directory/json/countryRegion/ + ${base_path}rest/default/V1/coupons/search GET true false @@ -2429,26 +2269,27 @@ idsList.add(vars.get("customer_id")); - - groovy + + javascript - import groovy.json.JsonSlurper -def jsonSlurper = new JsonSlurper(); -def regionResponse = jsonSlurper.parseText(prev.getResponseDataAsString()); + var data = JSON.parse(prev.getResponseDataAsString()); -regionResponse.each { region -> - if (region.label.toString() == "Alabama") { - props.put("alabama_region_id", region.value.toString()); - } else if (region.label.toString() == 'California') { - props.put("california_region_id", region.value.toString()); - } -} +var couponCodes = []; + +for (var i in data.items) { + var coupon = data.items[i]; + couponCodes.push({"coupon_id":coupon.coupon_id, "rule_id":coupon.rule_id, "code": coupon.code}); + } + +props.put("coupon_codes", couponCodes); + + Boolean stopTestOnError (String error) { @@ -2474,10 +2315,13 @@ if (props.get("customer_emails_list") == null) { return stopTestOnError("Cannot find customer emails. Test stopped."); } if (props.get("category_url_keys_list") == null) { - return stopTestOnError("Cannot find category url keys. Test stopped."); + return stopTestOnError("Cannot find category url keys. Test stopped."); } if (props.get("category_names_list") == null) { - return stopTestOnError("Cannot find category names. Test stopped."); + return stopTestOnError("Cannot find category names. Test stopped."); +} +if (props.get("cms_pages") == null) { + return stopTestOnError("Cannot find cms pages. Test stopped."); } @@ -2540,21 +2384,21 @@ if (props.get("category_names_list") == null) { - false + true ["customer_form_login"] = true blocks - false + true ["default","customer_account_login"] = true handles - false + true {"route":"customer","controller":"account","action":"login","uri":"/customer/account/login/"} = true @@ -2719,22 +2563,24 @@ vars.putObject("randomIntGenerator", random); - - -import java.util.Random; + + javascript + + + + random = vars.getObject("randomIntGenerator"); -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); +var categories = props.get("categories"); +number = random.nextInt(categories.length); -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + get-email mpaf/tool/fragments/ce/lock_controller.jmx @@ -3251,364 +3097,366 @@ vars.putObject("randomIntGenerator", random); - - -import java.util.Random; - -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); - -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); - - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path} - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/common/open_home_page.jmx - - - - <title>Home page</title> - - Assertion.response_data - false - 2 - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${category_url_key}${url_suffix} - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/common/open_category.jmx - - - - <span class="base" data-ui-id="page-title">${category_name}</span> - - Assertion.response_data - false - 6 - - - - false - category_id - <li class="item category([^'"]+)">\s*<strong>${category_name}</strong>\s*</li> - $1$ - - 1 - simple_product_1_url_key - - - - - ^[0-9]+$ - - Assertion.response_data - false - 1 - variable - category_id - - - - - - true - 2 - mpaf/tool/fragments/ce/loop_controller.jmx - - - 1 - - 1 - _counter - - true - true - - - - - -import java.util.Random; - -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("simple_products_list").size()); -product = props.get("simple_products_list").get(number); - -vars.put("product_url_key", product.get("url_key")); -vars.put("product_id", product.get("id")); -vars.put("product_name", product.get("title")); -vars.put("product_uenc", product.get("uenc")); -vars.put("product_sku", product.get("sku")); - - - - true - mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${product_url_key}${url_suffix} - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/product_view.jmx - - - - <span>In stock</span> - - Assertion.response_data - false - 2 - - - - - - - true - 1 - mpaf/tool/fragments/ce/loop_controller.jmx - - - 1 - - 1 - _counter - - true - true - - - - - -import java.util.Random; - -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("configurable_products_list").size()); -product = props.get("configurable_products_list").get(number); - -vars.put("product_url_key", product.get("url_key")); -vars.put("product_id", product.get("id")); -vars.put("product_name", product.get("title")); -vars.put("product_uenc", product.get("uenc")); -vars.put("product_sku", product.get("sku")); - - - - true - mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${product_url_key}${url_suffix} - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/product_view.jmx - - - - <span>In stock</span> - - Assertion.response_data - false - 2 - - - - - - - - - 1 - false - 1 - ${siteSearchPercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx - - - - vars.put("testLabel", "Site Search"); - - true - - - - - ${files_folder}search_terms.csv - UTF-8 - - , - false - true - false - shareMode.thread - mpaf/tool/fragments/ce/search/search_terms.jmx - - - + javascript - var cacheHitPercent = vars.get("cache_hits_percentage"); + random = vars.getObject("randomIntGenerator"); -if ( - cacheHitPercent < 100 && - sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - doCache(); -} +var categories = props.get("categories"); +number = random.nextInt(categories.length); -function doCache(){ - var random = Math.random() * 100; - if (cacheHitPercent < random) { - sampler.setPath(sampler.getPath() + "?cacheModifier=" + Math.random().toString(36).substring(2, 13)); - } -} - - mpaf/tool/fragments/ce/common/cache_hit_miss.jmx - - - - 1 - false - 1 - ${searchQuickPercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx - - - - vars.put("testLabel", "Quick Search"); - - true - - - - - - - 30 - ${host} - / - false - 0 - true - true - - - ${form_key} - ${host} - ${base_path} - false - 0 - true - true - - - true - mpaf/tool/fragments/ce/http_cookie_manager.jmx +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path} + GET + true + false + true + false + false + + mpaf/tool/fragments/ce/common/open_home_page.jmx + + + + <title>Home page</title> + + Assertion.response_data + false + 2 + + + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${category_url_key}${url_suffix} + GET + true + false + true + false + false + + mpaf/tool/fragments/ce/common/open_category.jmx + + + + <span class="base" data-ui-id="page-title">${category_name}</span> + + Assertion.response_data + false + 6 + + + + false + category_id + <li class="item category([^'"]+)">\s*<strong>${category_name}</strong>\s*</li> + $1$ + + 1 + simple_product_1_url_key + + + + + ^[0-9]+$ + + Assertion.response_data + false + 1 + variable + category_id + + + + + + true + 2 + mpaf/tool/fragments/ce/loop_controller.jmx + + + 1 + + 1 + _counter + + true + true + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${product_url_key}${url_suffix} + GET + true + false + true + false + false + + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/product_view.jmx + + + + <span>In stock</span> + + Assertion.response_data + false + 2 + + + + + + + true + 1 + mpaf/tool/fragments/ce/loop_controller.jmx + + + 1 + + 1 + _counter + + true + true + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${product_url_key}${url_suffix} + GET + true + false + true + false + false + + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/product_view.jmx + + + + <span>In stock</span> + + Assertion.response_data + false + 2 + + + + + + + + + 1 + false + 1 + ${siteSearchPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "Site Search"); + + true + + + + + ${files_folder}search_terms.csv + UTF-8 + + , + false + true + false + shareMode.thread + mpaf/tool/fragments/ce/search/search_terms.jmx + + + + javascript + + + + var cacheHitPercent = vars.get("cache_hits_percentage"); + +if ( + cacheHitPercent < 100 && + sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + doCache(); +} + +function doCache(){ + var random = Math.random() * 100; + if (cacheHitPercent < random) { + sampler.setPath(sampler.getPath() + "?cacheModifier=" + Math.random().toString(36).substring(2, 13)); + } +} + + mpaf/tool/fragments/ce/common/cache_hit_miss.jmx + + + + 1 + false + 1 + ${searchQuickPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "Quick Search"); + + true + + + + + + + 30 + ${host} + / + false + 0 + true + true + + + ${form_key} + ${host} + ${base_path} + false + 0 + true + true + + + true + mpaf/tool/fragments/ce/http_cookie_manager.jmx @@ -4656,22 +4504,24 @@ vars.put("totalProductsAdded", "0"); - - -import java.util.Random; + + javascript + + + + random = vars.getObject("randomIntGenerator"); -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); +var categories = props.get("categories"); +number = random.nextInt(categories.length); -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + @@ -5970,22 +5820,24 @@ vars.put("totalProductsAdded", "0"); - - -import java.util.Random; + + javascript + + + + random = vars.getObject("randomIntGenerator"); -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); +var categories = props.get("categories"); +number = random.nextInt(categories.length); -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + @@ -6537,22 +6389,24 @@ vars.put("totalProductsAdded", "0"); - - -import java.util.Random; + + javascript + + + + random = vars.getObject("randomIntGenerator"); -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); +var categories = props.get("categories"); +number = random.nextInt(categories.length); -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + @@ -7676,22 +7530,24 @@ vars.put("totalProductsAdded", "0"); - - -import java.util.Random; + + javascript + + + + random = vars.getObject("randomIntGenerator"); -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); +var categories = props.get("categories"); +number = random.nextInt(categories.length); -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + get-email mpaf/tool/fragments/ce/lock_controller.jmx @@ -9500,22 +9356,24 @@ vars.put("totalProductsAdded", "0"); - - -import java.util.Random; + + javascript + + + + random = vars.getObject("randomIntGenerator"); -Random random = vars.getObject("randomIntGenerator"); -number = random.nextInt(props.get("category_url_keys_list").size()); +var categories = props.get("categories"); +number = random.nextInt(categories.length); -vars.put("category_url_key", props.get("category_url_keys_list").get(number)); -vars.put("category_name", props.get("category_names_list").get(number)); +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); - - - false - mpaf/tool/fragments/ce/common/extract_category_setup.jmx - - + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + get-email mpaf/tool/fragments/ce/lock_controller.jmx @@ -27267,11 +27125,11 @@ if (testLabel mpaf/tool/fragments/_system/thread_group.jmx - + 1 false 1 - ${productGridMassActionPercentage} + ${importProductsPercentage} mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx @@ -27292,7 +27150,7 @@ if (testLabel - vars.put("testLabel", "Product Grid Mass Actions"); + vars.put("testLabel", "Import Products"); true @@ -27513,81 +27371,21 @@ vars.put("admin_user", adminUser); mpaf/tool/fragments/ce/simple_controller.jmx - + + vars.put("entity", "catalog_product"); +String behavior = "${adminImportProductBehavior}"; +vars.put("adminImportBehavior", behavior); +String filepath = "${files_folder}${adminImportProductFilePath}"; +vars.put("adminImportFilePath", filepath); + + + true + mpaf/tool/fragments/ce/import_products/setup.jmx + + + - - - true - ${admin_form_key} - = - true - form_key - - - true - product_listing - = - true - namespace - true - - - true - - = - true - search - true - - - true - true - = - true - filters[placeholder] - true - - - true - 20 - = - true - paging[pageSize] - true - - - true - 1 - = - true - paging[current] - true - - - true - entity_id - = - true - sorting[field] - true - - - true - asc - = - true - sorting[direction] - true - - - true - true - = - true - isAjax - true - - + @@ -27595,7 +27393,7 @@ vars.put("admin_user", adminUser); 200000 ${request_protocol} - ${base_path}${admin_path}/mui/index/render/ + ${base_path}${admin_path}/admin/import/ GET true false @@ -27603,78 +27401,20 @@ vars.put("admin_user", adminUser); false false - mpaf/tool/fragments/ce/admin_browse_products_grid/get_product_pages_count.jmx + mpaf/tool/fragments/ce/common/import.jmx - - $.totalRecords - 0 - true - false - true - - - - products_number - $.totalRecords - - - BODY - - - - false - - - var productsPageSize = Integer.parseInt(vars.get("products_page_size")); -var productsTotal = Integer.parseInt(vars.get("products_number")); -var pageCountProducts = Math.round(productsTotal/productsPageSize); - -vars.put("pages_count_product", String.valueOf(pageCountProducts)); - + + + Import Settings + + Assertion.response_data + false + 2 + - - -import java.util.Random; -Random random = new Random(); -if (${seedForRandom} > 0) { -random.setSeed(${seedForRandom}); -} -var productsPageSize = Integer.parseInt(vars.get("products_page_size")); -var totalNumberOfPages = Integer.parseInt(vars.get("pages_count_product")); - -// Randomly select a page. -var randomProductsPage = random.nextInt(totalNumberOfPages) + 1; - -// Get the first and last product id on that page. -var lastProductIdOnPage = randomProductsPage * productsPageSize; -var firstProductIdOnPage = lastProductIdOnPage - productsPageSize + 1; - -var randomProductId1 = Math.floor(random.nextInt(productsPageSize)) + firstProductIdOnPage; -var randomProductId2 = Math.floor(random.nextInt(productsPageSize)) + firstProductIdOnPage; -var randomProductId3 = Math.floor(random.nextInt(productsPageSize)) + firstProductIdOnPage; - -vars.put("page_number", String.valueOf(randomProductsPage)); -vars.put("productId1", String.valueOf(randomProductId1)); -vars.put("productId2", String.valueOf(randomProductId2)); -vars.put("productId3", String.valueOf(randomProductId3)); - -var randomQuantity = random.nextInt(1000) + 1; -var randomPrice = random.nextInt(500) + 10; -var randomVisibility = random.nextInt(4) + 1; - -vars.put("quantity", String.valueOf(randomQuantity)); -vars.put("price", String.valueOf(randomPrice)); -vars.put("visibility", String.valueOf(randomVisibility)); - - - - false - mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/setup.jmx - - - + @@ -27683,71 +27423,49 @@ vars.put("visibility", String.valueOf(randomVisibility)); = true form_key - true - - - true - product_listing - = - true - namespace - true - - - true - - = - true - search - true + false - + true - true + ${entity} = true - filters[placeholder] - true + entity - + true - ${products_page_size} + ${adminImportBehavior} = true - paging[pageSize] - true + behavior - + true - ${page_number} + validation-stop-on-errors = true - paging[current] - true + validation_strategy - + true - entity_id + 10 = true - sorting[field] - true + allowed_error_count - + true - asc + , = true - sorting[direction] - true + _import_field_separator - + true - true + , = true - isAjax - true + _import_multiple_value_separator @@ -27757,74 +27475,92 @@ vars.put("visibility", String.valueOf(randomVisibility)); 200000 ${request_protocol} - ${base_path}${admin_path}/mui/index/render/ - GET + ${base_path}${admin_path}/admin/import/validate + POST true false true false + HttpClient4 + + + + ${adminImportFilePath} + import_file + application/vnd.ms-excel + + + false - mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/display_grid.jmx + mpaf/tool/fragments/ce/common/import_validate.jmx - totalRecords + File is valid! To start import process Assertion.response_data false - 2 + 16 - + - + true - ${productId1} + ${admin_form_key} = true - selected[0] + form_key + false - + true - ${productId2} + ${entity} = true - selected[1] + entity - + true - ${productId3} + ${adminImportBehavior} = true - selected[2] - true + behavior - + true - true + validation-stop-on-errors = true - filters[placeholder] + validation_strategy false - + true - ${admin_form_key} + 10 = true - form_key + allowed_error_count false - + true - product_listing + , = true - namespace + _import_field_separator + false + + + true + , + = + true + _import_multiple_value_separator false @@ -27835,236 +27571,37 @@ vars.put("visibility", String.valueOf(randomVisibility)); 200000 ${request_protocol} - ${base_path}${admin_path}/catalog/product_action_attribute/edit - GET + ${base_path}${admin_path}/admin/import/start + POST true false true false + HttpClient4 + + + + ${adminImportFilePath} + import_file + application/vnd.ms-excel + + + false - mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/display_update_attributes.jmx + mpaf/tool/fragments/ce/common/import_save.jmx - Update Attributes + Import successfully done Assertion.response_data false - 2 + 16 - - - - - - true - true - = - true - isAjax - true - - - true - ${admin_form_key} - = - true - form_key - true - - - true - 1 - = - true - product[product_has_weight] - true - - - true - 1 - = - true - product[use_config_gift_message_available] - true - - - true - 1 - = - true - product[use_config_gift_wrapping_available] - true - - - true - ${quantity} - = - true - inventory[qty] - true - - - true - ${price} - = - true - attributes[price] - - - true - ${visibility} - = - true - attributes[visibility] - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/catalog/product_action_attribute/validate - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/admin_browse_products_grid/products_grid_mass_actions/change_attributes.jmx - - - - {"error":false} - - Assertion.response_data - false - 2 - - - - - - - - true - true - = - true - isAjax - false - - - true - ${admin_form_key} - = - true - form_key - false - - - true - 1 - = - true - product[product_has_weight] - true - - - true - 1 - = - true - product[use_config_gift_message_available] - - - true - 1 - = - true - product[use_config_gift_wrapping_available] - true - - - true - ${quantity} - = - true - inventory[qty] - - - true - on - = - true - toggle_price - true - - - true - ${price} - = - true - attributes[price] - - - true - on - = - true - toggle_price - true - - - true - ${visibility} - = - true - attributes[visibility] - - - true - on - = - true - toggle_visibility - true - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/catalog/product_action_attribute/save/store/0/active_tab/attributes - POST - true - false - true - true - false - - - - - - were updated. - - Assertion.response_data - false - 2 - - - @@ -28104,11 +27641,11 @@ vars.put("visibility", String.valueOf(randomVisibility)); - + 1 false 1 - ${importProductsPercentage} + ${importCustomersPercentage} mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx @@ -28129,7 +27666,7 @@ if (testLabel - vars.put("testLabel", "Import Products"); + vars.put("testLabel", "Import Customers"); true @@ -28346,22 +27883,22 @@ vars.put("admin_user", adminUser); - - mpaf/tool/fragments/ce/simple_controller.jmx - - - vars.put("entity", "catalog_product"); -String behavior = "${adminImportProductBehavior}"; + vars.put("entity", "customer"); +String behavior = "${adminImportCustomerBehavior}"; vars.put("adminImportBehavior", behavior); -String filepath = "${files_folder}${adminImportProductFilePath}"; +String filepath = "${files_folder}${adminImportCustomerFilePath}"; vars.put("adminImportFilePath", filepath); true - mpaf/tool/fragments/ce/import_products/setup.jmx + mpaf/tool/fragments/ce/import_customers/setup.jmx + + mpaf/tool/fragments/ce/simple_controller.jmx + + @@ -28620,11 +28157,11 @@ vars.put("adminImportFilePath", filepath); - + 1 false 1 - ${importCustomersPercentage} + ${apiBasePercentage} mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx @@ -28645,111 +28182,36 @@ if (testLabel - vars.put("testLabel", "Import Customers"); + vars.put("testLabel", "API"); true - - - function getFormKeyFromResponse() - { - var url = prev.getUrlAsString(), - responseCode = prev.getResponseCode(), - formKey = null; - searchPattern = /var FORM_KEY = '(.+)'/; - if (responseCode == "200" && url) { - response = prev.getResponseDataAsString(); - formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; - } - return formKey; - } - - formKey = vars.get("form_key_storage"); - - currentFormKey = getFormKeyFromResponse(); - - if (currentFormKey != null && currentFormKey != formKey) { - vars.put("form_key_storage", currentFormKey); - } - - javascript - mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx - - - - formKey = vars.get("form_key_storage"); - if (formKey - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' - && sampler.getMethod() == "POST") - { - arguments = sampler.getArguments(); - for (i=0; i<arguments.getArgumentCount(); i++) - { - argument = arguments.getArgument(i); - if (argument.getName() == 'form_key' && argument.getValue() != formKey) { - log.info("admin form key updated: " + argument.getValue() + " => " + formKey); - argument.setValue(formKey); - } - } - } - - javascript - - - - - - false - mpaf/tool/fragments/ce/http_cookie_manager_without_clear_each_iteration.jmx - - - - mpaf/tool/fragments/ce/simple_controller.jmx - - - - get-admin-email - mpaf/tool/fragments/ce/lock_controller.jmx - - - - mpaf/tool/fragments/ce/get_admin_email.jmx - -adminUserList = props.get("adminUserList"); -adminUserListIterator = props.get("adminUserListIterator"); -adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - -if (adminUsersDistribution == 1) { - adminUser = adminUserList.poll(); -} else { - if (!adminUserListIterator.hasNext()) { - adminUserListIterator = adminUserList.descendingIterator(); - } - - adminUser = adminUserListIterator.next(); -} - -if (adminUser == null) { - SampleResult.setResponseMessage("adminUser list is empty"); - SampleResult.setResponseData("adminUser list is empty","UTF-8"); - IsSuccess=false; - SampleResult.setSuccessful(false); - SampleResult.setStopThread(true); -} -vars.put("admin_user", adminUser); - - - - true - + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx - - - - + + true + + + + false + {"username":"${admin_user}","password":"${admin_password}"} + = + + @@ -28757,78 +28219,130 @@ vars.put("admin_user", adminUser); 200000 ${request_protocol} - ${base_path}${admin_path}/admin/ - GET + ${base_path}rest/V1/integration/admin/token + POST true false true false false - mpaf/tool/fragments/ce/admin_login/admin_login.jmx + mpaf/tool/fragments/ce/api/admin_token_retrieval.jmx - - - Welcome - <title>Magento Admin</title> - - Assertion.response_data - false - 2 - - - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - + + admin_token + $ + + + BODY + - + - ^.+$ + ^[a-z0-9-]+$ Assertion.response_data false 1 variable - admin_form_key + admin_token - + + + + Authorization + Bearer ${admin_token} + + + mpaf/tool/fragments/ce/api/header_manager.jmx + + + + 1 + false + 1 + 100 + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "API Process Orders"); + + true + + + + + // Each thread gets an equal number of orders, based on how many orders are available. + + int apiProcessOrders = Integer.parseInt("${apiProcessOrders}"); + if (apiProcessOrders > 0) { + ordersPerThread = apiProcessOrders; + } else { + ordersPerThread = 1; + } + + + threadNum = ${__threadNum}; + vars.put("ordersPerThread", String.valueOf(ordersPerThread)); + vars.put("threadNum", String.valueOf(threadNum)); + + + + + false + mpaf/tool/fragments/ce/api/process_orders/setup.jmx + + + - + true - + status = true - dummy + searchCriteria[filterGroups][0][filters][0][field] - + true - ${admin_form_key} + Pending = true - form_key + searchCriteria[filterGroups][0][filters][0][value] - + true - ${admin_password} + ${ordersPerThread} = true - login[password] + searchCriteria[pageSize] - + true - ${admin_user} + ${threadNum} = true - login[username] + searchCriteria[current_page] @@ -28838,47 +28352,33 @@ vars.put("admin_user", adminUser); 200000 ${request_protocol} - ${base_path}${admin_path}/admin/dashboard/ - POST + ${base_path}rest/default/V1/orders + GET true false true false - Java false - mpaf/tool/fragments/ce/admin_login/admin_login_submit_form.jmx - + mpaf/tool/fragments/ce/api/process_orders/get_orders.jmx - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - mpaf/tool/fragments/ce/admin_login/admin_retrieve_form_key.jmx - + + entity_ids + $.items[*].entity_id + + + BODY + + - - - - vars.put("entity", "customer"); -String behavior = "${adminImportCustomerBehavior}"; -vars.put("adminImportBehavior", behavior); -String filepath = "${files_folder}${adminImportCustomerFilePath}"; -vars.put("adminImportFilePath", filepath); - - - true - mpaf/tool/fragments/ce/import_customers/setup.jmx - - - mpaf/tool/fragments/ce/simple_controller.jmx - + + entity_ids + order_id + true + mpaf/tool/fragments/ce/api/process_orders/for_each_order.jmx - + @@ -28888,19 +28388,19 @@ vars.put("adminImportFilePath", filepath); 200000 ${request_protocol} - ${base_path}${admin_path}/admin/import/ - GET + ${base_path}rest/default/V1/order/${order_id}/invoice + POST true false true false false - mpaf/tool/fragments/ce/common/import.jmx + mpaf/tool/fragments/ce/api/process_orders/create_invoice.jmx - + - Import Settings + "\d+" Assertion.response_data false @@ -28909,60 +28409,9 @@ vars.put("adminImportFilePath", filepath); - + - - - true - ${admin_form_key} - = - true - form_key - false - - - true - ${entity} - = - true - entity - - - true - ${adminImportBehavior} - = - true - behavior - - - true - validation-stop-on-errors - = - true - validation_strategy - - - true - 10 - = - true - allowed_error_count - - - true - , - = - true - _import_field_separator - - - true - , - = - true - _import_multiple_value_separator - - + @@ -28970,93 +28419,75 @@ vars.put("adminImportFilePath", filepath); 200000 ${request_protocol} - ${base_path}${admin_path}/admin/import/validate + ${base_path}rest/default/V1/order/${order_id}/ship POST true false true false - HttpClient4 - - - - ${adminImportFilePath} - import_file - application/vnd.ms-excel - - - false - mpaf/tool/fragments/ce/common/import_validate.jmx + mpaf/tool/fragments/ce/api/process_orders/create_shipment.jmx - File is valid! To start import process + "\d+" Assertion.response_data false - 16 + 2 + + + - - + + 1 + false + 1 + 100 + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "API Product Attribute Management"); + + true + + + + + true + - - true - ${admin_form_key} - = - true - form_key - false - - - true - ${entity} - = - true - entity - - - true - ${adminImportBehavior} - = - true - behavior - - - true - validation-stop-on-errors - = - true - validation_strategy - false - - - true - 10 - = - true - allowed_error_count - false - - - true - , - = - true - _import_field_separator - false - - - true - , + + false + { + "attributeSet": { + "attribute_set_name": "new_attribute_set_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "sort_order": 500 + }, + "skeletonId": "4" +} = - true - _import_multiple_value_separator - false @@ -29066,41 +28497,113 @@ vars.put("adminImportFilePath", filepath); 200000 ${request_protocol} - ${base_path}${admin_path}/admin/import/start + ${base_path}rest/default/V1/products/attribute-sets/ POST true false true false - HttpClient4 - - - - ${adminImportFilePath} - import_file - application/vnd.ms-excel + false + + mpaf/tool/fragments/ce/api/create_attribute_set.jmx + + + attribute_set_id + $.attribute_set_id + + + BODY + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + attribute_set_id + + + + + + true + + + + false + { + "group": { + "attribute_group_name": "empty_attribute_group_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "attribute_set_id": ${attribute_set_id} + } +} + = + + + 60000 + 200000 + ${request_protocol} + + ${base_path}rest/default/V1/products/attribute-sets/groups + POST + true + false + true + false false - mpaf/tool/fragments/ce/common/import_save.jmx + mpaf/tool/fragments/ce/api/create_attribute_group.jmx - + + attribute_group_id + $.attribute_group_id + + + BODY + + + - Import successfully done + ^\d+$ Assertion.response_data false - 16 + 1 + variable + attribute_set_id - - - - + + true + + + + false + { + "attribute": { + "attribute_code": "attr_code_${__time()}", + "frontend_labels": [ + { + "store_id": 0, + "label": "front_lbl_${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)}" + } + ], + "default_value": "default value", + "frontend_input": "textarea", + "is_required": 1 + } +} + = + + @@ -29108,39 +28611,107 @@ vars.put("adminImportFilePath", filepath); 200000 ${request_protocol} - ${base_path}${admin_path}/admin/auth/logout/ - GET + ${base_path}rest/default/V1/products/attributes/ + POST true false true false false - mpaf/tool/fragments/ce/setup/admin_logout.jmx + mpaf/tool/fragments/ce/api/create_attribute.jmx - - - false - - - - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - - mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx - + + attribute_id + $.attribute_id + + + BODY + + + + attribute_code + $.attribute_code + + + BODY + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + attribute_id + + + + + ^[a-z0-9-_]+$ + + Assertion.response_data + false + 1 + variable + attribute_code + + + + + + true + + + + false + { + "attributeSetId": "${attribute_set_id}", + "attributeGroupId": "${attribute_group_id}", + "attributeCode": "${attribute_code}", + "sortOrder": 3 +} + = + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}rest/default/V1/products/attribute-sets/attributes + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/api/add_attribute_to_attribute_set.jmx + + + $ + (\d+) + true + false + false + + + + - + 1 false 1 - ${exportProductsPercentage} + ${adminCategoryManagementPercentage} mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx @@ -29161,7 +28732,7 @@ if (testLabel - vars.put("testLabel", "Export Products"); + vars.put("testLabel", "Admin Category Management"); true @@ -29382,1272 +28953,769 @@ vars.put("admin_user", adminUser); mpaf/tool/fragments/ce/simple_controller.jmx - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/export/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/common/export.jmx + + mpaf/tool/fragments/ce/admin_category_management/admin_category_management.jmx + - - - Export Settings - - Assertion.response_data - false - 2 - + + javascript + + + + random = new java.util.Random(); +if (${seedForRandom} > 0) { +random.setSeed(${seedForRandom} + ${__threadNum}); +} + +/** + * Get unique ids for fix concurrent category saving + */ +function getNextProductNumber(i) { + number = productsVariationsSize * ${__threadNum} - i; + if (number >= productsSize) { + log.info("${testLabel}: capacity of product list is not enough for support all ${adminPoolUsers} threads"); + return random.nextInt(productsSize); + } + return productsVariationsSize * ${__threadNum} - i; +} + +var productsVariationsSize = 5, + productsSize = props.get("simple_products_list_for_edit").size(); + + +for (i = 1; i<= productsVariationsSize; i++) { + var productVariablePrefix = "simple_product_" + i + "_"; + number = getNextProductNumber(i); + simpleList = props.get("simple_products_list_for_edit").get(number); + + vars.put(productVariablePrefix + "url_key", simpleList.get("url_key")); + vars.put(productVariablePrefix + "id", simpleList.get("id")); + vars.put(productVariablePrefix + "name", simpleList.get("title")); +} + +categoryIndex = random.nextInt(props.get("admin_category_ids_list").size()); +vars.put("parent_category_id", props.get("admin_category_ids_list").get(categoryIndex)); +do { +categoryIndexNew = random.nextInt(props.get("admin_category_ids_list").size()); +} while(categoryIndex == categoryIndexNew); +vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(categoryIndexNew)); + - - - - - - - true - form_key - ${admin_form_key} - = - true - - - true - attribute_code - - = - true - - - true - export_filter[allow_message][] - , - = - true - - - true - export_filter[allow_open_amount] - - = - true - - - true - export_filter[category_ids] - 24,25,26,27,28,29,30 - = - true - - - true - export_filter[configurable_variations] - - = - true - - - true - export_filter[cost][] - , - = - true - - - true - export_filter[country_of_manufacture] - - = - true - - - true - export_filter[created_at] - - = - true - - - true - export_filter[custom_design] - - = - true - - - true - export_filter[custom_design_from][] - , - = - true - - - true - export_filter[custom_design_to][] - , - = - true - - - true - export_filter[custom_layout_update] - - = - true - - - true - export_filter[description] - - = - true - - - true - export_filter[email_template] - - = - true - - - true - export_filter[gallery] - - = - true - - - true - export_filter[gift_message_available] - - = - true - - - true - export_filter[gift_wrapping_available] - - = - true - - - true - export_filter[gift_wrapping_price][] - , - = - true - - - true - export_filter[group_price][] - , - = - true - - - true - export_filter[has_options] - - = - true - - - true - export_filter[image] - - = - true - - - true - export_filter[image_label] - - = - true - - - true - export_filter[is_redeemable][] - , - = - true - - - true - export_filter[is_returnable] - - = - true - - - true - export_filter[lifetime][] - , - = - true - - - true - export_filter[links_exist][] - , - = - true - - - true - export_filter[links_purchased_separately][] - , - = - true - - - true - export_filter[links_title] - - = - true - - - true - export_filter[media_gallery] - - = - true - - - true - export_filter[meta_description] - - = - true - - - true - export_filter[meta_keyword] - - = - true - - - true - export_filter[meta_title] - - = - true - - - true - export_filter[minimal_price][] - , - = - true - - - true - export_filter[msrp][] - , - = - true - - - true - export_filter[msrp_display_actual_price_type] - - = - true - - - true - export_filter[name] - - = - true - - - true - export_filter[news_from_date][] - , - = - true - - - true - export_filter[news_to_date][] - , - = - true - - - true - export_filter[old_id][] - , - = - true - - - true - export_filter[open_amount_max][] - , - = - true - - - true - export_filter[open_amount_min][] - , - = - true - - - true - export_filter[options_container] - - = - true - - - true - export_filter[page_layout] - - = - true - - - true - export_filter[price][] - , - = - true - - - true - export_filter[price_type][] - , - = - true - - - true - export_filter[price_view] - - = - true - - - true - export_filter[quantity_and_stock_status] - - = - true - - - true - export_filter[related_tgtr_position_behavior][] - , - = - true - - - true - export_filter[related_tgtr_position_limit][] - , - = - true - - - true - export_filter[required_options] - - = - true - - - true - export_filter[samples_title] - - = - true - - - true - export_filter[shipment_type][] - , - = - true - - - true - export_filter[short_description] - - = - true - - - true - export_filter[sku] - - = - true - - - true - export_filter[sku_type][] - , - = - true - - - true - export_filter[small_image] - - = - true - - - true - export_filter[small_image_label] - - = - true - - - true - export_filter[special_from_date][] - , - = - true - - - true - export_filter[special_price][] - , - = - true - - - true - export_filter[special_to_date][] - , - = - true - - - true - export_filter[status] - - = - true - - - true - export_filter[tax_class_id] - - = - true - - - true - export_filter[thumbnail] - - = - true - - - true - export_filter[thumbnail_label] - - = - true - - - true - export_filter[tier_price][] - , - = - true - - - true - export_filter[updated_at] - - = - true - - - true - export_filter[upsell_tgtr_position_behavior][] - , - = - true - - - true - export_filter[upsell_tgtr_position_limit][] - , - = - true - - - true - export_filter[url_key] - - = - true - - - true - export_filter[url_path] - - = - true - - - true - export_filter[use_config_allow_message][] - , - = - true - - - true - export_filter[use_config_email_template][] - , - = - true - - - true - export_filter[use_config_is_redeemable][] - , - = - true - - - true - export_filter[use_config_lifetime][] - , - = - true - - - true - export_filter[visibility] - - = - true - - - true - export_filter[weight][] - , - = - true - - - true - export_filter[weight_type][] - , - = - true - - - true - frontend_label - - = - true - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/export/export/entity/catalog_product/file_format/csv - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/export_products/export_products.jmx - - - - Simple Product 1 - - Assertion.response_data - false - 16 - - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/auth/logout/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/setup/admin_logout.jmx - - - - false - - - - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - - mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx - - - - - - - 1 - false - 1 - ${exportCustomersPercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/ + GET + true + false + true + false + false + + + + + + + Accept-Language + en-US,en;q=0.5 + + + Accept + text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + + + User-Agent + Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 + + + Accept-Encoding + gzip, deflate + + + - - - vars.put("testLabel", "Export Customers"); - - true - + + false + admin_form_key + <input name="form_key" type="hidden" value="([^'"]+)" /> + $1$ + + 1 + - - - - function getFormKeyFromResponse() - { - var url = prev.getUrlAsString(), - responseCode = prev.getResponseCode(), - formKey = null; - searchPattern = /var FORM_KEY = '(.+)'/; - if (responseCode == "200" && url) { - response = prev.getResponseDataAsString(); - formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; - } - return formKey; - } - - formKey = vars.get("form_key_storage"); - - currentFormKey = getFormKeyFromResponse(); - - if (currentFormKey != null && currentFormKey != formKey) { - vars.put("form_key_storage", currentFormKey); - } - - javascript - mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/edit/id/${parent_category_id}/ + GET + true + false + true + false + false + + + + + + + Accept-Language + en-US,en;q=0.5 + + + Accept + text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + + + User-Agent + Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 + + + Accept-Encoding + gzip, deflate + + + - - - formKey = vars.get("form_key_storage"); - if (formKey - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' - && sampler.getMethod() == "POST") - { - arguments = sampler.getArguments(); - for (i=0; i<arguments.getArgumentCount(); i++) - { - argument = arguments.getArgument(i); - if (argument.getName() == 'form_key' && argument.getValue() != formKey) { - log.info("admin form key updated: " + argument.getValue() + " => " + formKey); - argument.setValue(formKey); - } - } - } - - javascript - + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/add/store/0/parent/${parent_category_id} + GET + true + false + true + false + false + + + + + + <title>New Category + + Assertion.response_data + false + 2 + - - - - false - mpaf/tool/fragments/ce/http_cookie_manager_without_clear_each_iteration.jmx - - - - mpaf/tool/fragments/ce/simple_controller.jmx - - - - get-admin-email - mpaf/tool/fragments/ce/lock_controller.jmx - - - - mpaf/tool/fragments/ce/get_admin_email.jmx - -adminUserList = props.get("adminUserList"); -adminUserListIterator = props.get("adminUserListIterator"); -adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - -if (adminUsersDistribution == 1) { - adminUser = adminUserList.poll(); -} else { - if (!adminUserListIterator.hasNext()) { - adminUserListIterator = adminUserList.descendingIterator(); - } - - adminUser = adminUserListIterator.next(); -} - -if (adminUser == null) { - SampleResult.setResponseMessage("adminUser list is empty"); - SampleResult.setResponseData("adminUser list is empty","UTF-8"); - IsSuccess=false; - SampleResult.setSuccessful(false); - SampleResult.setStopThread(true); -} -vars.put("admin_user", adminUser); - - - - true - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/admin_login/admin_login.jmx - - - - Welcome - <title>Magento Admin</title> - - Assertion.response_data - false - 2 - - - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_form_key - - - - - - - - - true - - = - true - dummy - - - true - ${admin_form_key} - = - true - form_key - - - true - ${admin_password} - = - true - login[password] - - - true - ${admin_user} - = - true - login[username] - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/dashboard/ - POST - true - false - true - false - Java - false - - mpaf/tool/fragments/ce/admin_login/admin_login_submit_form.jmx - - - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - mpaf/tool/fragments/ce/admin_login/admin_retrieve_form_key.jmx + + + + + + true + + = + true + id + + + true + ${parent_category_id} + = + true + parent + + + true + + = + true + path + + + true + + = + true + store_id + + + true + 0 + = + true + is_active + + + true + 0 + = + true + include_in_menu + + + true + 1 + = + true + is_anchor + + + true + true + = + true + use_config[available_sort_by] + + + true + true + = + true + use_config[default_sort_by] + + + true + true + = + true + use_config[filter_price_range] + + + true + false + = + true + use_default[url_key] + + + true + 0 + = + true + url_key_create_redirect + + + true + 0 + = + true + custom_use_parent_settings + + + true + 0 + = + true + custom_apply_to_products + + + true + Admin Category Management ${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} + = + true + name + + + true + admin-category-management-${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} + = + true + url_key + + + true + + = + true + meta_title + + + true + + = + true + description + + + true + PRODUCTS + = + true + display_mode + + + true + position + = + true + default_sort_by + + + true + + = + true + meta_keywords + + + true + + = + true + meta_description + + + true + + = + true + custom_layout_update + + + false + {"${simple_product_1_id}":"","${simple_product_2_id}":"","${simple_product_3_id}":"","${simple_product_4_id}":"","${simple_product_5_id}":""} + = + true + category_products + + + true + ${admin_form_key} + = + true + form_key + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/save/ + POST + true + false + true + false + false + + + + + URL + admin_category_id + /catalog/category/edit/id/(\d+)/ + $1$ + + 1 + - - - - - mpaf/tool/fragments/ce/simple_controller.jmx - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/export/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/common/export.jmx - - - - Export Settings - - Assertion.response_data - false - 2 - - - - - - - - - true - ${admin_form_key} - = - true - form_key - false - - - true - - = - true - attribute_code - true - - - true - - = - true - export_filter[confirmation] - true - - - true - - = - true - export_filter[created_at] - true - - - true - - = - true - export_filter[created_in] - true - - - true - , - = - true - export_filter[default_billing][] - true - - - true - , - = - true - export_filter[default_shipping][] - true - - - true - - = - true - export_filter[disable_auto_group_change] - true - - - true - , - = - true - export_filter[dob][] - true - - - true - - = - true - export_filter[email] - true - - - true - - = - true - export_filter[firstname] - true - - - true - - = - true - export_filter[gender] - true - - - true - - = - true - export_filter[group_id] - true - - - true - - = - true - export_filter[lastname] - true - - - true - - = - true - export_filter[middlename] - true - - - true - - = - true - export_filter[password_hash] - true - - - true - - = - true - export_filter[prefix] - true - - - true - , - = - true - export_filter[reward_update_notification][] - true - - - true - , - = - true - export_filter[reward_warning_notification][] - true - - - true - - = - true - export_filter[rp_token] - true - - - true - , - = - true - export_filter[rp_token_created_at][] - true - - - true - - = - true - export_filter[store_id] - true - - - true - - = - true - export_filter[suffix] - true - - - true - - = - true - export_filter[taxvat] - true - - - true - - = - true - export_filter[website_id] - true - - - true - - = - true - frontend_label - true - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/export/export/entity/customer/file_format/csv - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/export_customers/export_customers.jmx - - - - user_1@example.com - - Assertion.response_data - false - 16 - - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/auth/logout/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/setup/admin_logout.jmx - - - - false - - - - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - - mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx - - - - - - - 1 - false - 1 - ${apiBasePercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + admin_category_id + - - - vars.put("testLabel", "API"); - - true - + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/edit/id/${admin_category_id}/ + GET + true + false + true + false + false + + + + + + + Accept-Language + en-US,en;q=0.5 + + + Accept + text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + + + User-Agent + Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 + + + Accept-Encoding + gzip, deflate + + + - - - - - Content-Type - application/json + + false + admin_category_entity_id + "entity_id":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_attribute_set_id + "attribute_set_id":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_parent_id + "parent_id":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_created_at + "created_at":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_updated_at + "updated_at":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_path + "entity_id":(.+)"path":"([^\"]+)" + $2$ + + 1 + + + + false + admin_category_level + "level":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_name + "entity_id":(.+)"name":"([^"]+)" + $2$ + + 1 + + + + false + admin_category_url_key + "url_key":"([^"]+)" + $1$ + + 1 + + + + false + admin_category_url_path + "url_path":"([^"]+)" + $1$ + + 1 + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + admin_category_entity_id + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + admin_category_attribute_set_id + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + admin_category_parent_id + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_category_created_at + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_category_updated_at + + + + + ^[\d\\\/]+$ + + Assertion.response_data + false + 1 + variable + admin_category_path + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + admin_category_level + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_category_name + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_category_url_key + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_category_url_path + + + + + ${simple_product_1_name} + ${simple_product_2_name} + ${simple_product_3_name} + ${simple_product_4_name} + ${simple_product_5_name} + + Assertion.response_data + false + 2 + + + + + + + + true + ${admin_category_id} + = + true + id + + + true + ${admin_form_key} + = + true + form_key + + + true + append + = + true + point + + + true + ${new_parent_category_id} + = + true + pid + + + true + ${parent_category_id} + = + true + paid + + + true + 0 + = + true + aid + + + true + true + = + true + isAjax + + - - Accept - */* + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/move/ + POST + true + false + true + false + false + + + + + + + + true + ${admin_form_key} + = + true + form_key + + - - mpaf/tool/fragments/ce/api/header_manager_before_token.jmx - + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/catalog/category/delete/id/${admin_category_id}/ + POST + true + false + true + false + false + + + + + + You deleted the category. + + Assertion.response_data + false + 2 + + + + + 1 + 0 + ${__javaScript(Math.round(${adminCategoryManagementDelay}*1000))} + + + + - - true - - - - false - {"username":"${admin_user}","password":"${admin_password}"} - = - - + + + @@ -30655,52 +29723,39 @@ if (testLabel 200000 ${request_protocol} - ${base_path}rest/V1/integration/admin/token - POST + ${base_path}${admin_path}/admin/auth/logout/ + GET true false true false false - mpaf/tool/fragments/ce/api/admin_token_retrieval.jmx + mpaf/tool/fragments/ce/setup/admin_logout.jmx - - admin_token - $ - - - BODY - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - admin_token - - - - - - - - Authorization - Bearer ${admin_token} - - - mpaf/tool/fragments/ce/api/header_manager.jmx + + + false + + + + adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); + if (adminUsersDistribution == 1) { + adminUserList = props.get("adminUserList"); + adminUserList.add(vars.get("admin_user")); + } + + mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx + + + - + 1 false 1 - 100 + ${adminPromotionRulesPercentage} mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx @@ -30721,454 +29776,7 @@ if (testLabel - vars.put("testLabel", "API Process Orders"); - - true - - - - - // Each thread gets an equal number of orders, based on how many orders are available. - - int apiProcessOrders = Integer.parseInt("${apiProcessOrders}"); - if (apiProcessOrders > 0) { - ordersPerThread = apiProcessOrders; - } else { - ordersPerThread = 1; - } - - - threadNum = ${__threadNum}; - vars.put("ordersPerThread", String.valueOf(ordersPerThread)); - vars.put("threadNum", String.valueOf(threadNum)); - - - - - false - mpaf/tool/fragments/ce/api/process_orders/setup.jmx - - - - - - - true - status - = - true - searchCriteria[filterGroups][0][filters][0][field] - - - true - Pending - = - true - searchCriteria[filterGroups][0][filters][0][value] - - - true - ${ordersPerThread} - = - true - searchCriteria[pageSize] - - - true - ${threadNum} - = - true - searchCriteria[current_page] - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/orders - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/api/process_orders/get_orders.jmx - - - entity_ids - $.items[*].entity_id - - - BODY - - - - - - entity_ids - order_id - true - mpaf/tool/fragments/ce/api/process_orders/for_each_order.jmx - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/order/${order_id}/invoice - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/process_orders/create_invoice.jmx - - - - "\d+" - - Assertion.response_data - false - 2 - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/order/${order_id}/ship - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/process_orders/create_shipment.jmx - - - - "\d+" - - Assertion.response_data - false - 2 - - - - - - - - - 1 - false - 1 - 100 - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx - - - - vars.put("testLabel", "API Product Attribute Management"); - - true - - - - - true - - - - false - { - "attributeSet": { - "attribute_set_name": "new_attribute_set_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "sort_order": 500 - }, - "skeletonId": "4" -} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/products/attribute-sets/ - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/create_attribute_set.jmx - - - attribute_set_id - $.attribute_set_id - - - BODY - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - attribute_set_id - - - - - - true - - - - false - { - "group": { - "attribute_group_name": "empty_attribute_group_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "attribute_set_id": ${attribute_set_id} - } -} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/products/attribute-sets/groups - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/create_attribute_group.jmx - - - attribute_group_id - $.attribute_group_id - - - BODY - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - attribute_set_id - - - - - - true - - - - false - { - "attribute": { - "attribute_code": "attr_code_${__time()}", - "frontend_labels": [ - { - "store_id": 0, - "label": "front_lbl_${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)}" - } - ], - "default_value": "default value", - "frontend_input": "textarea", - "is_required": 1 - } -} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/products/attributes/ - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/create_attribute.jmx - - - attribute_id - $.attribute_id - - - BODY - - - - attribute_code - $.attribute_code - - - BODY - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - attribute_id - - - - - ^[a-z0-9-_]+$ - - Assertion.response_data - false - 1 - variable - attribute_code - - - - - - true - - - - false - { - "attributeSetId": "${attribute_set_id}", - "attributeGroupId": "${attribute_group_id}", - "attributeCode": "${attribute_code}", - "sortOrder": 3 -} - = - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}rest/default/V1/products/attribute-sets/attributes - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/add_attribute_to_attribute_set.jmx - - - $ - (\d+) - true - false - false - - - - - - - - - - 1 - false - 1 - ${adminCategoryManagementPercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx - - - - vars.put("testLabel", "Admin Category Management"); + vars.put("testLabel", "Admin Promotion Rules"); true @@ -31389,54 +29997,10 @@ vars.put("admin_user", adminUser); mpaf/tool/fragments/ce/simple_controller.jmx - - mpaf/tool/fragments/ce/admin_category_management/admin_category_management.jmx + + mpaf/tool/fragments/ce/admin_promotions_management/admin_promotions_management.jmx - - javascript - - - - random = new java.util.Random(); -if (${seedForRandom} > 0) { -random.setSeed(${seedForRandom} + ${__threadNum}); -} - -/** - * Get unique ids for fix concurrent category saving - */ -function getNextProductNumber(i) { - number = productsVariationsSize * ${__threadNum} - i; - if (number >= productsSize) { - log.info("${testLabel}: capacity of product list is not enough for support all ${adminPoolUsers} threads"); - return random.nextInt(productsSize); - } - return productsVariationsSize * ${__threadNum} - i; -} - -var productsVariationsSize = 5, - productsSize = props.get("simple_products_list_for_edit").size(); - - -for (i = 1; i<= productsVariationsSize; i++) { - var productVariablePrefix = "simple_product_" + i + "_"; - number = getNextProductNumber(i); - simpleList = props.get("simple_products_list_for_edit").get(number); - - vars.put(productVariablePrefix + "url_key", simpleList.get("url_key")); - vars.put(productVariablePrefix + "id", simpleList.get("id")); - vars.put(productVariablePrefix + "name", simpleList.get("title")); -} - -categoryIndex = random.nextInt(props.get("admin_category_ids_list").size()); -vars.put("parent_category_id", props.get("admin_category_ids_list").get(categoryIndex)); -do { -categoryIndexNew = random.nextInt(props.get("admin_category_ids_list").size()); -} while(categoryIndex == categoryIndexNew); -vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(categoryIndexNew)); - - @@ -31447,7 +30011,7 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate 200000 ${request_protocol} - ${base_path}${admin_path}/catalog/category/ + ${base_path}${admin_path}/sales_rule/promo_quote/ GET true false @@ -31456,39 +30020,8 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate false - - - - - Accept-Language - en-US,en;q=0.5 - - - Accept - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 - - - User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 - - - Accept-Encoding - gzip, deflate - - - - - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - - - - + + @@ -31498,7 +30031,7 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate 200000 ${request_protocol} - ${base_path}${admin_path}/catalog/category/edit/id/${parent_category_id}/ + ${base_path}${admin_path}/sales_rule/promo_quote/new GET true false @@ -31507,32 +30040,40 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate false - - - - - Accept-Language - en-US,en;q=0.5 + + + + + + true + true + = + true + isAjax - - Accept - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + + true + ${admin_form_key} + = + true + form_key + true - - User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 + + true + 1--1 + = + true + id - - Accept-Encoding - gzip, deflate + + true + Magento\SalesRule\Model\Rule\Condition\Address|base_subtotal + = + true + type - - - - - - @@ -31540,8 +30081,8 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate 200000 ${request_protocol} - ${base_path}${admin_path}/catalog/category/add/store/0/parent/${parent_category_id} - GET + ${base_path}${admin_path}/sales_rule/promo_quote/newConditionHtml/form/sales_rule_formrule_conditions_fieldset_/form_namespace/sales_rule_form + POST true false true @@ -31549,187 +30090,268 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate false - - - - <title>New Category - - Assertion.response_data - false - 2 - - - - + + - + + true + Rule Name ${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} + = + true + name + + + true + 0 + = + true + is_active + + + true + 0 + = + true + use_auto_generation + + + true + 1 + = + true + is_rss + + + true + 0 + = + true + apply_to_shipping + + + true + 0 + = + true + stop_rules_processing + + true = true - id + coupon_code - + true - ${parent_category_id} + = true - parent + uses_per_coupon - + true = true - path + uses_per_customer - + true = true - store_id + sort_order - + true - 0 + 5 = true - is_active + discount_amount - + true 0 = true - include_in_menu + discount_qty - + true - 1 + = true - is_anchor + discount_step - + true - true + = true - use_config[available_sort_by] + reward_points_delta - + true - true + = true - use_config[default_sort_by] + store_labels[0] - + true - true + Rule Description ${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} = true - use_config[filter_price_range] + description - + true - false + 1 = true - use_default[url_key] + coupon_type - + true - 0 + cart_fixed = true - url_key_create_redirect + simple_action - + true - 0 + 1 = true - custom_use_parent_settings + website_ids[0] - + true 0 = true - custom_apply_to_products + customer_group_ids[0] - + true - Admin Category Management ${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} + = true - name + from_date - + true - admin-category-management-${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} + = true - url_key + to_date - + true - + Magento\SalesRule\Model\Rule\Condition\Combine = true - meta_title + rule[conditions][1][type] - + + true + all + = + true + rule[conditions][1][aggregator] + + + true + 1 + = + true + rule[conditions][1][value] + + + true + Magento\SalesRule\Model\Rule\Condition\Address + = + true + rule[conditions][1--1][type] + + + true + base_subtotal + = + true + rule[conditions][1--1][attribute] + + + true + >= + = + true + rule[conditions][1--1][operator] + + + true + 100 + = + true + rule[conditions][1--1][value] + + true = true - description + rule[conditions][1][new_chlid] - + true - PRODUCTS + Magento\SalesRule\Model\Rule\Condition\Product\Combine = true - display_mode + rule[actions][1][type] - + true - position + all = true - default_sort_by + rule[actions][1][aggregator] - + + true + 1 + = + true + rule[actions][1][value] + + true = true - meta_keywords + rule[actions][1][new_child] - + true = true - meta_description + store_labels[1] - + true = true - custom_layout_update + store_labels[2] - - false - {"${simple_product_1_id}":"","${simple_product_2_id}":"","${simple_product_3_id}":"","${simple_product_4_id}":"","${simple_product_5_id}":""} + + true + = true - category_products + related_banners true @@ -31746,7 +30368,7 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate 200000 ${request_protocol} - ${base_path}${admin_path}/catalog/category/save/ + ${base_path}${admin_path}/sales_rule/promo_quote/save/ POST true false @@ -31756,514 +30378,140 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate - - URL - admin_category_id - /catalog/category/edit/id/(\d+)/ - $1$ - - 1 - - - + - ^\d+$ + You saved the rule. Assertion.response_data false - 1 - variable - admin_category_id + 16 - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/catalog/category/edit/id/${admin_category_id}/ - GET - true - false - true - false - false - - - - - - - Accept-Language - en-US,en;q=0.5 - - - Accept - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 - - - User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 - - - Accept-Encoding - gzip, deflate - - - - - - false - admin_category_entity_id - "entity_id":"([^"]+)" - $1$ - - 1 - + + 1 + 0 + ${__javaScript(Math.round(${adminPromotionsManagementDelay}*1000))} + + + + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/auth/logout/ + GET + true + false + true + false + false + + mpaf/tool/fragments/ce/setup/admin_logout.jmx + + + + false + + + + adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); + if (adminUsersDistribution == 1) { + adminUserList = props.get("adminUserList"); + adminUserList.add(vars.get("admin_user")); + } + + mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx + + + + + + + 1 + false + 1 + ${adminCustomerManagementPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx - - false - admin_category_attribute_set_id - "attribute_set_id":"([^"]+)" - $1$ - - 1 - + + + vars.put("testLabel", "Admin Customer Management"); + + true + - - false - admin_category_parent_id - "parent_id":"([^"]+)" - $1$ - - 1 - + + + + function getFormKeyFromResponse() + { + var url = prev.getUrlAsString(), + responseCode = prev.getResponseCode(), + formKey = null; + searchPattern = /var FORM_KEY = '(.+)'/; + if (responseCode == "200" && url) { + response = prev.getResponseDataAsString(); + formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; + } + return formKey; + } + + formKey = vars.get("form_key_storage"); + + currentFormKey = getFormKeyFromResponse(); + + if (currentFormKey != null && currentFormKey != formKey) { + vars.put("form_key_storage", currentFormKey); + } + + javascript + mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx - - false - admin_category_created_at - "created_at":"([^"]+)" - $1$ - - 1 - - - - false - admin_category_updated_at - "updated_at":"([^"]+)" - $1$ - - 1 - - - - false - admin_category_path - "entity_id":(.+)"path":"([^\"]+)" - $2$ - - 1 - - - - false - admin_category_level - "level":"([^"]+)" - $1$ - - 1 - - - - false - admin_category_name - "entity_id":(.+)"name":"([^"]+)" - $2$ - - 1 - - - - false - admin_category_url_key - "url_key":"([^"]+)" - $1$ - - 1 - - - - false - admin_category_url_path - "url_path":"([^"]+)" - $1$ - - 1 - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - admin_category_entity_id - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - admin_category_attribute_set_id - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - admin_category_parent_id - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_category_created_at - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_category_updated_at - - - - - ^[\d\\\/]+$ - - Assertion.response_data - false - 1 - variable - admin_category_path - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - admin_category_level - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_category_name - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_category_url_key - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_category_url_path - - - - - ${simple_product_1_name} - ${simple_product_2_name} - ${simple_product_3_name} - ${simple_product_4_name} - ${simple_product_5_name} - - Assertion.response_data - false - 2 - - - - - - - - true - ${admin_category_id} - = - true - id - - - true - ${admin_form_key} - = - true - form_key - - - true - append - = - true - point - - - true - ${new_parent_category_id} - = - true - pid - - - true - ${parent_category_id} - = - true - paid - - - true - 0 - = - true - aid - - - true - true - = - true - isAjax - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/catalog/category/move/ - POST - true - false - true - false - false - - - - - - - - true - ${admin_form_key} - = - true - form_key - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/catalog/category/delete/id/${admin_category_id}/ - POST - true - false - true - false - false - - - - - - You deleted the category. - - Assertion.response_data - false - 2 - - - - - 1 - 0 - ${__javaScript(Math.round(${adminCategoryManagementDelay}*1000))} - - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/auth/logout/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/setup/admin_logout.jmx - - - - false - - - - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - - mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx - - - - - - - 1 - false - 1 - ${adminPromotionRulesPercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx - - - - vars.put("testLabel", "Admin Promotion Rules"); - - true - - - - - - function getFormKeyFromResponse() - { - var url = prev.getUrlAsString(), - responseCode = prev.getResponseCode(), - formKey = null; - searchPattern = /var FORM_KEY = '(.+)'/; - if (responseCode == "200" && url) { - response = prev.getResponseDataAsString(); - formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; - } - return formKey; - } - - formKey = vars.get("form_key_storage"); - - currentFormKey = getFormKeyFromResponse(); - - if (currentFormKey != null && currentFormKey != formKey) { - vars.put("form_key_storage", currentFormKey); - } - - javascript - mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx - - - - formKey = vars.get("form_key_storage"); - if (formKey - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' - && sampler.getMethod() == "POST") - { - arguments = sampler.getArguments(); - for (i=0; i<arguments.getArgumentCount(); i++) - { - argument = arguments.getArgument(i); - if (argument.getName() == 'form_key' && argument.getValue() != formKey) { - log.info("admin form key updated: " + argument.getValue() + " => " + formKey); - argument.setValue(formKey); - } - } - } - - javascript - + + + formKey = vars.get("form_key_storage"); + if (formKey + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' + && sampler.getMethod() == "POST") + { + arguments = sampler.getArguments(); + for (i=0; i<arguments.getArgumentCount(); i++) + { + argument = arguments.getArgument(i); + if (argument.getName() == 'form_key' && argument.getValue() != formKey) { + log.info("admin form key updated: " + argument.getValue() + " => " + formKey); + argument.setValue(formKey); + } + } + } + + javascript + @@ -32433,8 +30681,8 @@ vars.put("admin_user", adminUser); mpaf/tool/fragments/ce/simple_controller.jmx - - mpaf/tool/fragments/ce/admin_promotions_management/admin_promotions_management.jmx + + mpaf/tool/fragments/ce/admin_customer_management/admin_customer_management.jmx @@ -32447,27 +30695,7 @@ vars.put("admin_user", adminUser); 200000 ${request_protocol} - ${base_path}${admin_path}/sales_rule/promo_quote/ - GET - true - false - true - false - false - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/sales_rule/promo_quote/new + ${base_path}${admin_path}/customer/index GET true false @@ -32476,325 +30704,174 @@ vars.put("admin_user", adminUser); false - - + + + + + Accept-Language + en-US,en;q=0.5 + + + Accept + text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + + + User-Agent + Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 + + + Accept-Encoding + gzip, deflate + + + + + + - + true - true + customer_listing = true - isAjax + namespace - + true - ${admin_form_key} + = true - form_key - true + search - + true - 1--1 + true = true - id + filters[placeholder] - + true - Magento\SalesRule\Model\Rule\Condition\Address|base_subtotal + 20 = true - type + paging[pageSize] - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/sales_rule/promo_quote/newConditionHtml/form/sales_rule_formrule_conditions_fieldset_/form_namespace/sales_rule_form - POST - true - false - true - false - false - - - - - - - + true - Rule Name ${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} + 1 = true - name - - - true - 0 - = - true - is_active - - - true - 0 - = - true - use_auto_generation - - - true - 1 - = - true - is_rss - - - true - 0 - = - true - apply_to_shipping - - - true - 0 - = - true - stop_rules_processing - - - true - - = - true - coupon_code - - - true - - = - true - uses_per_coupon - - - true - - = - true - uses_per_customer - - - true - - = - true - sort_order - - - true - 5 - = - true - discount_amount - - - true - 0 - = - true - discount_qty - - - true - - = - true - discount_step - - - true - - = - true - reward_points_delta - - - true - - = - true - store_labels[0] - - - true - Rule Description ${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)} - = - true - description - - - true - 1 - = - true - coupon_type - - - true - cart_fixed - = - true - simple_action - - - true - 1 - = - true - website_ids[0] - - - true - 0 - = - true - customer_group_ids[0] - - - true - - = - true - from_date - - - true - - = - true - to_date - - - true - Magento\SalesRule\Model\Rule\Condition\Combine - = - true - rule[conditions][1][type] - - - true - all - = - true - rule[conditions][1][aggregator] + paging[current] - + true - 1 + entity_id = true - rule[conditions][1][value] + sorting[field] - + true - Magento\SalesRule\Model\Rule\Condition\Address + asc = true - rule[conditions][1--1][type] + sorting[direction] - + true - base_subtotal + true = true - rule[conditions][1--1][attribute] + isAjax - - true - >= - = - true - rule[conditions][1--1][operator] + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/mui/index/render/ + GET + true + false + true + false + false + + + + + + + X-Requested-With + XMLHttpRequest - + + + + + + + + true - 100 + customer_listing = true - rule[conditions][1--1][value] + namespace - + true - + Lastname = true - rule[conditions][1][new_chlid] + search - + true - Magento\SalesRule\Model\Rule\Condition\Product\Combine + true = true - rule[actions][1][type] + filters[placeholder] - + true - all + 20 = true - rule[actions][1][aggregator] + paging[pageSize] - + true 1 = true - rule[actions][1][value] - - - true - - = - true - rule[actions][1][new_child] - - - true - - = - true - store_labels[1] + paging[current] - + true - + entity_id = true - store_labels[2] + sorting[field] - + true - + asc = true - related_banners + sorting[direction] - + true - ${admin_form_key} + true = true - form_key + isAjax @@ -32804,8 +30881,8 @@ vars.put("admin_user", adminUser); 200000 ${request_protocol} - ${base_path}${admin_path}/sales_rule/promo_quote/save/ - POST + ${base_path}${admin_path}/mui/index/render/ + GET true false true @@ -32814,604 +30891,91 @@ vars.put("admin_user", adminUser); - + + + + X-Requested-With + XMLHttpRequest + + + + + + false + customer_edit_url_path + actions":\{"edit":\{"href":"(?:http|https):\\/\\/(.*?)\\/customer\\/index\\/edit\\/id\\/(\d+)\\/", + /customer/index/edit/id/$2$/ + + 1 + + + - You saved the rule. + ^.+$ Assertion.response_data false - 16 + 1 + variable + customer_edit_url_path - - 1 - 0 - ${__javaScript(Math.round(${adminPromotionsManagementDelay}*1000))} - - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/auth/logout/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/setup/admin_logout.jmx - - - - false - - - - adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - if (adminUsersDistribution == 1) { - adminUserList = props.get("adminUserList"); - adminUserList.add(vars.get("admin_user")); - } - - mpaf/tool/fragments/ce/common/return_admin_email_to_pool.jmx - - - - - - - 1 - false - 1 - ${adminCustomerManagementPercentage} - mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx - - - -var testLabel = "${testLabel}" ? " (${testLabel})" : ""; -if (testLabel - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' -) { - if (sampler.getName().indexOf(testLabel) == -1) { - sampler.setName(sampler.getName() + testLabel); - } -} else if (sampler.getName().indexOf("SetUp - ") == -1) { - sampler.setName("SetUp - " + sampler.getName()); -} - - javascript - mpaf/tool/fragments/_system/setup_label.jmx + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}${customer_edit_url_path} + GET + true + false + true + false + false + + + + + + Customer Information + + Assertion.response_data + false + 2 + - - - vars.put("testLabel", "Admin Customer Management"); - - true - + + false + admin_customer_entity_id + "entity_id":"(\d+)" + $1$ + + 1 + - - - - function getFormKeyFromResponse() - { - var url = prev.getUrlAsString(), - responseCode = prev.getResponseCode(), - formKey = null; - searchPattern = /var FORM_KEY = '(.+)'/; - if (responseCode == "200" && url) { - response = prev.getResponseDataAsString(); - formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; - } - return formKey; - } - - formKey = vars.get("form_key_storage"); - - currentFormKey = getFormKeyFromResponse(); - - if (currentFormKey != null && currentFormKey != formKey) { - vars.put("form_key_storage", currentFormKey); - } - - javascript - mpaf/tool/fragments/ce/admin/handle_admin_form_key.jmx + + false + admin_customer_website_id + "website_id":"(\d+)" + $1$ + + 1 + - - - formKey = vars.get("form_key_storage"); - if (formKey - && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' - && sampler.getMethod() == "POST") - { - arguments = sampler.getArguments(); - for (i=0; i<arguments.getArgumentCount(); i++) - { - argument = arguments.getArgument(i); - if (argument.getName() == 'form_key' && argument.getValue() != formKey) { - log.info("admin form key updated: " + argument.getValue() + " => " + formKey); - argument.setValue(formKey); - } - } - } - - javascript - - - - - - false - mpaf/tool/fragments/ce/http_cookie_manager_without_clear_each_iteration.jmx - - - - mpaf/tool/fragments/ce/simple_controller.jmx - - - - get-admin-email - mpaf/tool/fragments/ce/lock_controller.jmx - - - - mpaf/tool/fragments/ce/get_admin_email.jmx - -adminUserList = props.get("adminUserList"); -adminUserListIterator = props.get("adminUserListIterator"); -adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); - -if (adminUsersDistribution == 1) { - adminUser = adminUserList.poll(); -} else { - if (!adminUserListIterator.hasNext()) { - adminUserListIterator = adminUserList.descendingIterator(); - } - - adminUser = adminUserListIterator.next(); -} - -if (adminUser == null) { - SampleResult.setResponseMessage("adminUser list is empty"); - SampleResult.setResponseData("adminUser list is empty","UTF-8"); - IsSuccess=false; - SampleResult.setSuccessful(false); - SampleResult.setStopThread(true); -} -vars.put("admin_user", adminUser); - - - - true - - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/ - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/admin_login/admin_login.jmx - - - - Welcome - <title>Magento Admin</title> - - Assertion.response_data - false - 2 - - - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - admin_form_key - - - - - - - - - true - - = - true - dummy - - - true - ${admin_form_key} - = - true - form_key - - - true - ${admin_password} - = - true - login[password] - - - true - ${admin_user} - = - true - login[username] - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/dashboard/ - POST - true - false - true - false - Java - false - - mpaf/tool/fragments/ce/admin_login/admin_login_submit_form.jmx - - - - false - admin_form_key - <input name="form_key" type="hidden" value="([^'"]+)" /> - $1$ - - 1 - mpaf/tool/fragments/ce/admin_login/admin_retrieve_form_key.jmx - - - - - - mpaf/tool/fragments/ce/simple_controller.jmx - - - - mpaf/tool/fragments/ce/admin_customer_management/admin_customer_management.jmx - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/customer/index - GET - true - false - true - false - false - - - - - - - Accept-Language - en-US,en;q=0.5 - - - Accept - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 - - - User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 - - - Accept-Encoding - gzip, deflate - - - - - - - - - - true - customer_listing - = - true - namespace - - - true - - = - true - search - - - true - true - = - true - filters[placeholder] - - - true - 20 - = - true - paging[pageSize] - - - true - 1 - = - true - paging[current] - - - true - entity_id - = - true - sorting[field] - - - true - asc - = - true - sorting[direction] - - - true - true - = - true - isAjax - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/mui/index/render/ - GET - true - false - true - false - false - - - - - - - X-Requested-With - XMLHttpRequest - - - - - - - - - - true - customer_listing - = - true - namespace - - - true - Lastname - = - true - search - - - true - true - = - true - filters[placeholder] - - - true - 20 - = - true - paging[pageSize] - - - true - 1 - = - true - paging[current] - - - true - entity_id - = - true - sorting[field] - - - true - asc - = - true - sorting[direction] - - - true - true - = - true - isAjax - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/mui/index/render/ - GET - true - false - true - false - false - - - - - - - X-Requested-With - XMLHttpRequest - - - - - - false - customer_edit_url_path - actions":\{"edit":\{"href":"(?:http|https):\\/\\/(.*?)\\/customer\\/index\\/edit\\/id\\/(\d+)\\/", - /customer/index/edit/id/$2$/ - - 1 - - - - - ^.+$ - - Assertion.response_data - false - 1 - variable - customer_edit_url_path - - - - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}${customer_edit_url_path} - GET - true - false - true - false - false - - - - - - Customer Information - - Assertion.response_data - false - 2 - - - - false - admin_customer_entity_id - "entity_id":"(\d+)" - $1$ - - 1 - - - - false - admin_customer_website_id - "website_id":"(\d+)" - $1$ - - 1 - - - - false - admin_customer_firstname - "firstname":"([^"]+)" - $1$ - - 1 - + + false + admin_customer_firstname + "firstname":"([^"]+)" + $1$ + + 1 + false @@ -40375,349 +37939,5322 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu - - true - - - - false - { - "product": { - "sku": "apsku-test-${__time()}-${__threadNum}-${__Random(1,1000000)}", - "name": "Extensible_Product_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "visibility": "4", - "type_id": "virtual", - "price": "3.62", - "status": "1", - "attribute_set_id": "4", - "custom_attributes": [ - { - "attribute_code": "cost", - "value": "" - }, - { - "attribute_code": "description", - "value": "Description" - } - ], - "extension_attributes":{ - "stock_item":{ - "manage_stock": 1, - "is_in_stock": 1, - "qty":"100" - } - } , - "media_gallery_entries": - [{ - "id": null, - "label":"test_label_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "position":1, - "disabled":0, - "media_type":"image", - "types":["image"], - "content":{ - "base64_encoded_data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iioLy8t9Ps5bu7lWKCIZd26KKaTbshpX0RPRXN/8J/4V/6DVv8Ak3+FH/Cf+Ff+g1b/AJN/hXR9SxP/AD7l9zNPYVf5X9x0lFc3/wAJ/wCFf+g1b/k3+FH/AAn/AIV/6DVv+Tf4UfUsT/z7l9zD2FX+V/cdJRXN/wDCf+Ff+g1b/k3+FH/Cf+Ff+g1b/k3+FH1LE/8APuX3MPYVf5X9x0lFc3/wn/hX/oNW/wCTf4Uf8J/4V/6DVv8Ak3+FH1LE/wDPuX3MPYVf5X9x0lFVdP1G01WyS8sZ1nt3JCyL0ODg/qKtVzyi4u0lZmbTTswrm/H3/Iiav/1x/wDZhXSVzfj7/kRNX/64/wDswrowf+80/wDEvzNKH8WPqj5voorB1zS7OLT7m7SHE5YNu3HqWGeM471+kYutOhSdSEU7Jt3dtF20f6H1FacqcHJK9vO36M3qKzTa6foqPdxwlWxswrFi2T0AJ9aRdVmjkT7XYSW8TsFEm8MAT0yB0qfrcafu1tJeV2l2u7K3zsL2yjpPR+V3+NjTorPn1GVbt7a1s2uJIwDJ84ULnpyaik1SWTTrp47Z0uIQRJGzAFOPvZ70Sx1GLau9L9H03SdrNrsgdeCuu3k+hq0VR0ma4msImuIih2LtYvuLjA+b2zV6uijUVWmprqaQkpxUl1PoP4Xf8iBYf78v/oxq7GuO+F3/ACIFh/vy/wDoxq7GvzTMf98q/wCJ/mfLYn+NP1YVzfj7/kRNX/64/wDswrpK5vx9/wAiJq//AFx/9mFRg/8Aeaf+JfmTQ/ix9UfN9ZniD/kB3H/Af/QhWnTZI45kKSIroeqsMg1+l4mk61GdNfaTX3o+pqw54Sj3Rma/GXsI3BcLFMruU+8F5yR+dUZ4tOeNFOq3tx5jACNZg5J+mK6PrUMdrbxPvjgiR/7yoAa48TgPa1HNW1STvfp2s1+JjVw/PJy017mbe/YTqTB7iWzuQgPmhtocfjwajiupbjTtTieUXCxRsqTKMb8qePwrYlghnAE0UcgHQOoP86ckaRoERFVR/CowKbwU3UclJJO+19brqr203vvoHsJczd7J3/H8PmVNJnhm063WOVHZIkDhTkqcd/yNXajighg3eTFHHu67FAz+VSV2UIShTjGe67G9NOMUpbn0H8Lv+RAsP9+X/wBGNXY1x3wu/wCRAsP9+X/0Y1djX5tmP++Vf8T/ADPl8T/Gn6sK5vx9/wAiJq//AFx/9mFdJXN+Pv8AkRNX/wCuP/swqMH/ALzT/wAS/Mmh/Fj6o+b6KKK/Uj60KKKKACiiigAooooA+g/hd/yIFh/vy/8Aoxq7GuO+F3/IgWH+/L/6Mauxr8wzH/fKv+J/mfKYn+NP1YVzfj7/AJETV/8Arj/7MK6Sub8ff8iJq/8A1x/9mFRg/wDeaf8AiX5k0P4sfVHzfRRRX6kfWhRRRQAUUUUAFFFFAH0H8Lv+RAsP9+X/ANGNXY1x3wu/5ECw/wB+X/0Y1djX5hmP++Vf8T/M+UxP8afqwqC8s7fULOW0u4llglGHRujCp6K5E2ndGKdtUc3/AMIB4V/6Atv+bf40f8IB4V/6Atv+bf410lFdH13E/wDPyX3s09vV/mf3nN/8IB4V/wCgLb/m3+NH/CAeFf8AoC2/5t/jXSUUfXcT/wA/Jfew9vV/mf3nN/8ACAeFf+gLb/m3+NH/AAgHhX/oC2/5t/jXSUUfXcT/AM/Jfew9vV/mf3nN/wDCAeFf+gLb/m3+NH/CAeFf+gLb/m3+NdJRR9dxP/PyX3sPb1f5n95V0/TrTSrJLOxgWC3QkrGvQZOT+pq1RRXPKTk7yd2Zttu7P//Z", - "type": "image/jpeg", - "name": "test_image_${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)}.jpeg" - } - } - ] - } - } - = - - - - - - - - ${request_protocol} - - ${base_path}rest/default/V1/products - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/create_virtual_product_with_extensible_data_objects.jmx - - - - virtual_product_id - $.id - - - BODY - - - - virtual_product_sku - $.sku - - - BODY - - - - virtual_stock_item_id - $.extension_attributes.stock_item.item_id - - - BODY - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - virtual_product_id - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - virtual_product_sku - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - virtual_stock_item_id - - - + + true + + + + false + { + "product": { + "sku": "apsku-test-${__time()}-${__threadNum}-${__Random(1,1000000)}", + "name": "Extensible_Product_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "visibility": "4", + "type_id": "virtual", + "price": "3.62", + "status": "1", + "attribute_set_id": "4", + "custom_attributes": [ + { + "attribute_code": "cost", + "value": "" + }, + { + "attribute_code": "description", + "value": "Description" + } + ], + "extension_attributes":{ + "stock_item":{ + "manage_stock": 1, + "is_in_stock": 1, + "qty":"100" + } + } , + "media_gallery_entries": + [{ + "id": null, + "label":"test_label_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "position":1, + "disabled":0, + "media_type":"image", + "types":["image"], + "content":{ + "base64_encoded_data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iioLy8t9Ps5bu7lWKCIZd26KKaTbshpX0RPRXN/8J/4V/6DVv8Ak3+FH/Cf+Ff+g1b/AJN/hXR9SxP/AD7l9zNPYVf5X9x0lFc3/wAJ/wCFf+g1b/k3+FH/AAn/AIV/6DVv+Tf4UfUsT/z7l9zD2FX+V/cdJRXN/wDCf+Ff+g1b/k3+FH/Cf+Ff+g1b/k3+FH1LE/8APuX3MPYVf5X9x0lFc3/wn/hX/oNW/wCTf4Uf8J/4V/6DVv8Ak3+FH1LE/wDPuX3MPYVf5X9x0lFVdP1G01WyS8sZ1nt3JCyL0ODg/qKtVzyi4u0lZmbTTswrm/H3/Iiav/1x/wDZhXSVzfj7/kRNX/64/wDswrowf+80/wDEvzNKH8WPqj5voorB1zS7OLT7m7SHE5YNu3HqWGeM471+kYutOhSdSEU7Jt3dtF20f6H1FacqcHJK9vO36M3qKzTa6foqPdxwlWxswrFi2T0AJ9aRdVmjkT7XYSW8TsFEm8MAT0yB0qfrcafu1tJeV2l2u7K3zsL2yjpPR+V3+NjTorPn1GVbt7a1s2uJIwDJ84ULnpyaik1SWTTrp47Z0uIQRJGzAFOPvZ70Sx1GLau9L9H03SdrNrsgdeCuu3k+hq0VR0ma4msImuIih2LtYvuLjA+b2zV6uijUVWmprqaQkpxUl1PoP4Xf8iBYf78v/oxq7GuO+F3/ACIFh/vy/wDoxq7GvzTMf98q/wCJ/mfLYn+NP1YVzfj7/kRNX/64/wDswrpK5vx9/wAiJq//AFx/9mFRg/8Aeaf+JfmTQ/ix9UfN9ZniD/kB3H/Af/QhWnTZI45kKSIroeqsMg1+l4mk61GdNfaTX3o+pqw54Sj3Rma/GXsI3BcLFMruU+8F5yR+dUZ4tOeNFOq3tx5jACNZg5J+mK6PrUMdrbxPvjgiR/7yoAa48TgPa1HNW1STvfp2s1+JjVw/PJy017mbe/YTqTB7iWzuQgPmhtocfjwajiupbjTtTieUXCxRsqTKMb8qePwrYlghnAE0UcgHQOoP86ckaRoERFVR/CowKbwU3UclJJO+19brqr203vvoHsJczd7J3/H8PmVNJnhm063WOVHZIkDhTkqcd/yNXajighg3eTFHHu67FAz+VSV2UIShTjGe67G9NOMUpbn0H8Lv+RAsP9+X/wBGNXY1x3wu/wCRAsP9+X/0Y1djX5tmP++Vf8T/ADPl8T/Gn6sK5vx9/wAiJq//AFx/9mFdJXN+Pv8AkRNX/wCuP/swqMH/ALzT/wAS/Mmh/Fj6o+b6KKK/Uj60KKKKACiiigAooooA+g/hd/yIFh/vy/8Aoxq7GuO+F3/IgWH+/L/6Mauxr8wzH/fKv+J/mfKYn+NP1YVzfj7/AJETV/8Arj/7MK6Sub8ff8iJq/8A1x/9mFRg/wDeaf8AiX5k0P4sfVHzfRRRX6kfWhRRRQAUUUUAFFFFAH0H8Lv+RAsP9+X/ANGNXY1x3wu/5ECw/wB+X/0Y1djX5hmP++Vf8T/M+UxP8afqwqC8s7fULOW0u4llglGHRujCp6K5E2ndGKdtUc3/AMIB4V/6Atv+bf40f8IB4V/6Atv+bf410lFdH13E/wDPyX3s09vV/mf3nN/8IB4V/wCgLb/m3+NH/CAeFf8AoC2/5t/jXSUUfXcT/wA/Jfew9vV/mf3nN/8ACAeFf+gLb/m3+NH/AAgHhX/oC2/5t/jXSUUfXcT/AM/Jfew9vV/mf3nN/wDCAeFf+gLb/m3+NH/CAeFf+gLb/m3+NdJRR9dxP/PyX3sPb1f5n95V0/TrTSrJLOxgWC3QkrGvQZOT+pq1RRXPKTk7yd2Zttu7P//Z", + "type": "image/jpeg", + "name": "test_image_${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)}.jpeg" + } + } + ] + } + } + = + + + + + + + + ${request_protocol} + + ${base_path}rest/default/V1/products + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/api/create_virtual_product_with_extensible_data_objects.jmx + + + + virtual_product_id + $.id + + + BODY + + + + virtual_product_sku + $.sku + + + BODY + + + + virtual_stock_item_id + $.extension_attributes.stock_item.item_id + + + BODY + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + virtual_product_id + + + + + ^[a-z0-9-]+$ + + Assertion.response_data + false + 1 + variable + virtual_product_sku + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + virtual_stock_item_id + + + + + + mpaf/tool/fragments/ce/api/create_grouped_product_with_extensible_data_objects.jmx + + + + true + + + + false + { + "product": { + "sku": "apsku-test-${__time()}-${__threadNum}-${__Random(1,1000000)}", + "name": "Extensible_Product_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "visibility": "4", + "type_id": "grouped", + "price": "3.62", + "status": "1", + "attribute_set_id": "4", + "custom_attributes": [ + { + "attribute_code": "cost", + "value": "" + }, + { + "attribute_code": "description", + "value": "Description" + } + ], + "extension_attributes":{ + "stock_item":{ + "manage_stock": 1, + "is_in_stock": 1, + "qty":"100" + } + } , + "media_gallery_entries": + [{ + "id": null, + "label":"test_label_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "position":1, + "disabled":false, + "media_type":"image", + "types":["image"], + "content":{ + "base64_encoded_data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iioLy8t9Ps5bu7lWKCIZd26KKaTbshpX0RPRXN/8J/4V/6DVv8Ak3+FH/Cf+Ff+g1b/AJN/hXR9SxP/AD7l9zNPYVf5X9x0lFc3/wAJ/wCFf+g1b/k3+FH/AAn/AIV/6DVv+Tf4UfUsT/z7l9zD2FX+V/cdJRXN/wDCf+Ff+g1b/k3+FH/Cf+Ff+g1b/k3+FH1LE/8APuX3MPYVf5X9x0lFc3/wn/hX/oNW/wCTf4Uf8J/4V/6DVv8Ak3+FH1LE/wDPuX3MPYVf5X9x0lFVdP1G01WyS8sZ1nt3JCyL0ODg/qKtVzyi4u0lZmbTTswrm/H3/Iiav/1x/wDZhXSVzfj7/kRNX/64/wDswrowf+80/wDEvzNKH8WPqj5voorB1zS7OLT7m7SHE5YNu3HqWGeM471+kYutOhSdSEU7Jt3dtF20f6H1FacqcHJK9vO36M3qKzTa6foqPdxwlWxswrFi2T0AJ9aRdVmjkT7XYSW8TsFEm8MAT0yB0qfrcafu1tJeV2l2u7K3zsL2yjpPR+V3+NjTorPn1GVbt7a1s2uJIwDJ84ULnpyaik1SWTTrp47Z0uIQRJGzAFOPvZ70Sx1GLau9L9H03SdrNrsgdeCuu3k+hq0VR0ma4msImuIih2LtYvuLjA+b2zV6uijUVWmprqaQkpxUl1PoP4Xf8iBYf78v/oxq7GuO+F3/ACIFh/vy/wDoxq7GvzTMf98q/wCJ/mfLYn+NP1YVzfj7/kRNX/64/wDswrpK5vx9/wAiJq//AFx/9mFRg/8Aeaf+JfmTQ/ix9UfN9ZniD/kB3H/Af/QhWnTZI45kKSIroeqsMg1+l4mk61GdNfaTX3o+pqw54Sj3Rma/GXsI3BcLFMruU+8F5yR+dUZ4tOeNFOq3tx5jACNZg5J+mK6PrUMdrbxPvjgiR/7yoAa48TgPa1HNW1STvfp2s1+JjVw/PJy017mbe/YTqTB7iWzuQgPmhtocfjwajiupbjTtTieUXCxRsqTKMb8qePwrYlghnAE0UcgHQOoP86ckaRoERFVR/CowKbwU3UclJJO+19brqr203vvoHsJczd7J3/H8PmVNJnhm063WOVHZIkDhTkqcd/yNXajighg3eTFHHu67FAz+VSV2UIShTjGe67G9NOMUpbn0H8Lv+RAsP9+X/wBGNXY1x3wu/wCRAsP9+X/0Y1djX5tmP++Vf8T/ADPl8T/Gn6sK5vx9/wAiJq//AFx/9mFdJXN+Pv8AkRNX/wCuP/swqMH/ALzT/wAS/Mmh/Fj6o+b6KKK/Uj60KKKKACiiigAooooA+g/hd/yIFh/vy/8Aoxq7GuO+F3/IgWH+/L/6Mauxr8wzH/fKv+J/mfKYn+NP1YVzfj7/AJETV/8Arj/7MK6Sub8ff8iJq/8A1x/9mFRg/wDeaf8AiX5k0P4sfVHzfRRRX6kfWhRRRQAUUUUAFFFFAH0H8Lv+RAsP9+X/ANGNXY1x3wu/5ECw/wB+X/0Y1djX5hmP++Vf8T/M+UxP8afqwqC8s7fULOW0u4llglGHRujCp6K5E2ndGKdtUc3/AMIB4V/6Atv+bf40f8IB4V/6Atv+bf410lFdH13E/wDPyX3s09vV/mf3nN/8IB4V/wCgLb/m3+NH/CAeFf8AoC2/5t/jXSUUfXcT/wA/Jfew9vV/mf3nN/8ACAeFf+gLb/m3+NH/AAgHhX/oC2/5t/jXSUUfXcT/AM/Jfew9vV/mf3nN/wDCAeFf+gLb/m3+NH/CAeFf+gLb/m3+NdJRR9dxP/PyX3sPb1f5n95V0/TrTSrJLOxgWC3QkrGvQZOT+pq1RRXPKTk7yd2Zttu7P//Z", + "type": "image/jpeg", + "name": "test_image_${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)}.jpeg" + } + } + ] + } + } + = + + + + + + + + ${request_protocol} + + ${base_path}rest/default/V1/products + POST + true + false + true + false + false + + + + + grouped_product_id + $.id + + + BODY + + + + grouped_product_sku + $.sku + + + BODY + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + grouped_product_id + + + + + ^[a-z0-9-]+$ + + Assertion.response_data + false + 1 + variable + grouped_product_sku + + + + + + + true + + + + false + { + "items": [ + { + "sku": "${grouped_product_sku}", + "link_type": "associated", + "linked_product_sku": "${bundle_product_sku}", + "linked_product_type": "bundle", + "position": 1, + "extension_attributes": { + "qty": 1 + } + }, + { + "sku": "${grouped_product_sku}", + "link_type": "associated", + "linked_product_sku": "${configurable_product_sku}", + "linked_product_type": "configurable", + "position": 2, + "extension_attributes": { + "qty": 1 + } + }, + { + "sku": "${grouped_product_sku}", + "link_type": "associated", + "linked_product_sku": "${simple_product_sku}", + "linked_product_type": "simple", + "position": 3, + "extension_attributes": { + "qty": 1 + } + }, + { + "sku": "${grouped_product_sku}", + "link_type": "associated", + "linked_product_sku": "${downloadable_product_sku}", + "linked_product_type": "downloadable", + "position": 4, + "extension_attributes": { + "qty": 1 + } + } + ] + } + + = + + + + + + + + ${request_protocol} + + ${base_path}rest/default/V1/products/${grouped_product_sku}/links + POST + true + false + true + false + false + + + + + grouped_product_response + $ + + + BODY + + + + + true + + Assertion.response_data + false + 8 + variable + grouped_product_response + + + + + + + + + + mpaf/tool/fragments/ce/simple_controller.jmx + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + true + + + + false + {"username":"${admin_user}","password":"${admin_password}"} + = + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}rest/V1/integration/admin/token + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/api/admin_token_retrieval.jmx + + + admin_token + $ + + + BODY + + + + + ^[a-z0-9-]+$ + + Assertion.response_data + false + 1 + variable + admin_token + + + + + + + + Authorization + Bearer ${admin_token} + + + mpaf/tool/fragments/ce/api/header_manager.jmx + + + + true + + + + false + {"query":"{\n products(\n filter: {\n price: {gt: \"10\"}\n or: {\n sku:{like:\"%Product%\"}\n name:{like:\"%Configurable Product%\"}\n }\n }\n pageSize: 20\n currentPage: 0\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_filter_only.jmx + + + + graphql_multiple_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_multiple_products_query_total_count"); + if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; + } else { + if (Integer.parseInt(totalCount) < 200) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than 200, Actual: " + totalCount; + } else { + Failure = false; + } + } + + + + false + + + + + + true + + + + false + {"query":"{\n products(filter: {sku: { eq: \"${simple_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_simple_product_with_extensible_data_objects.jmx + + + + graphql_simple_products_query_total_count + $.data.products.total_count + + + BODY + + + + + graphql_multiple_products_query_response + $ + + + BODY + + + + String totalCount=vars.get("graphql_simple_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) != 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + false + + + + $.data.products.items[0].sku + ${simple_product_sku} + true + false + false + true + + + + + + true + + + + false + {"query":"{\n products(filter: {sku: {eq:\"${configurable_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_configurable_product_with_extensible_data_objects.jmx + + + + graphql_configurable_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_configurable_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) != 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + + + false + + + + $.data.products.items[0].sku + ${configurable_product_sku} + true + false + false + true + + + + + + true + + + + false + {"query":"{\n products(\n pageSize:20\n currentPage:0\n search: \"configurable\"\n filter: {name: {like: \"Configurable Product%\"} }\n ) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_and_filter.jmx + + + + graphql_search_products_query_total_count_fulltext_filter + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_search_products_query_total_count_fulltext_filter"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + + + false + + + + + + true + + + + false + {"query":"{\n products(\n pageSize:20\n currentPage:0\n search: \"configurable\") {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_only.jmx + + + + graphql_search_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_search_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + + + false + + + + + + true + + + + false + {"query":"{\n products(\n pageSize:20\n currentPage:0\n search: \"Option 1\") {\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n items_count\n ... on SwatchLayerFilterItemInterface {\n swatch_data {\n type\n value\n }\n }\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_and_filters.jmx + + + + graphql_search_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_search_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + + + false + + + + + + true + + + + false + {"query":"{\nproducts(filter: {sku: {eq:\"${bundle_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on BundleProduct {\n weight\n price_view\n dynamic_price\n dynamic_sku\n ship_bundle_items\n dynamic_weight\n items {\n option_id\n title\n required\n type\n position\n sku\n options {\n id\n qty\n position\n is_default\n price\n price_type\n can_change_quantity\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_bundle_product_with_extensible_data_objects.jmx + + + + graphql_bundle_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_bundle_products_query_total_count"); + + if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; + } else { + if (Integer.parseInt(totalCount) != 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; + } else { + Failure = false; + } + } + + + + + + false + + + + $.data.products.items[0].sku + ${bundle_product_sku} + true + false + false + false + + + + + + true + + + + false + {"query":"{\n products(filter: {sku: { eq: \"${downloadable_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n ... on DownloadableProduct {\n links_purchased_separately\n links_title\n downloadable_product_samples {\n id\n title\n sort_order\n sample_type\n sample_file\n sample_url\n }\n downloadable_product_links {\n id\n title\n sort_order\n is_shareable\n price\n number_of_downloads\n link_type\n sample_type\n sample_file\n sample_url\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_downloadable_product_with_extensible_data_objects.jmx + + + + graphql_downloadable_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_downloadable_products_query_total_count"); + + if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; + } else { + if (Integer.parseInt(totalCount) != 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; + } else { + Failure = false; + } + } + + + + + + false + + + + $.data.products.items[0].sku + ${downloadable_product_sku} + true + false + false + true + + + + $.data.products.items[0].downloadable_product_samples..title + ["sample1","sample2"] + true + false + false + false + + + + $.data.products.items[0].downloadable_product_samples..title + ["sample1","sample2"] + true + false + false + false + + + + + + true + + + + false + {"query":"{\n products(filter: {sku: { eq: \"${virtual_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_virtual_product_with_extensible_data_objects.jmx + + + + graphql_virtual_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_virtual_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) != 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + false + + + + $.data.products.items[0].sku + ${virtual_product_sku} + true + false + false + true + + + + + + true + + + + false + {"query":"{\nproducts(filter: {sku: {eq:\"${grouped_product_sku}\"} }) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on GroupedProduct {\n weight\n items {\n qty\n position\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_grouped_product_with_extensible_data_objects.jmx + + + + graphql_grouped_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_grouped_products_query_total_count"); + + if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; + } else { + if (Integer.parseInt(totalCount) != 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; + } else { + Failure = false; + } + } + + + + + + false + + + + $.data.products.items[0].sku + ${grouped_product_sku} + true + false + false + true + + + + + + + + true + + + + false + { + "customer": { + + "email": "customer_${__time()}-${__threadNum}-${__Random(1,1000000)}@example.com", + "firstname": "test_${__time()}-${__threadNum}-${__Random(1,1000000)}", + "lastname": "Doe" + }, + "password": "test@123" + } + = + + + + + + + + ${request_protocol} + + ${base_path}rest/default/V1/customers + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_frontend_customer.jmx + + + + customer_id + $.id + + + BODY + + + + + ^\d+$ + + Assertion.response_data + false + 1 + variable + customer_id + + + + + + + + + + + + ${request_protocol} + + ${base_path}rest/default/V1/customers/${customer_id} + GET + true + false + true + false + false + + mpaf/tool/fragments/ce/api/check_customer.jmx + + + + $.id + ${customer_id} + true + false + false + true + + + + customer_email + $.email + + + BODY + + + + + true + + + + false + { + "username":"${customer_email}", + "password":"test@123" + } + + = + + + + + + + + ${request_protocol} + + ${base_path}rest/default/V1/integration/customer/token + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/api/create_customer.jmx + + + + customer_token + $ + + + BODY + + + + + true + + + + false + {"query":"{\n customer {\n created_at\n group_id\n\n prefix\n firstname\n middlename\n lastname\n suffix\n email\n default_billing\n default_shipping\n\n dob\n taxvat\n\n id\n addresses {\n id\n customer_id\n region {\n region_code\n region\n region_id\n }\n region_id\n country_id\n street \n company\n telephone\n fax\n postcode\n city\n firstname\n lastname\n middlename\n prefix\n suffix\n vat_id\n default_shipping\n default_billing\n }\n is_subscribed\n }\n}","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_frontend_customer.jmx + + + + false + + + import org.apache.jmeter.protocol.http.control.Header; + + sampler.getHeaderManager().removeHeaderNamed("Authorization"); + + sampler.getHeaderManager().add(new Header("Authorization","Bearer " + vars.get("customer_token"))); + + + + $.data.customer.lastname + Doe + true + false + false + true + + + + + + + true + + + + false + {"query":"{\n category(id: 1) {\n name\n id\n level\n description\n path\n path_in_store\n url_key\n url_path\n children {\n id\n description\n default_sort_by\n children {\n id\n description\n level\n children {\n level\n id\n children {\n id\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + + + + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/query_root_category.jmx + + + + graphql_category_query_name + $.data.category.name + + + BODY + + + + String name = vars.get("graphql_category_query_name"); +if (name == null) { + Failure = true; + FailureMessage = "Not Expected \"children\" to be null"; +} else { + if (!name.equals("Root Catalog")) { + Failure = true; + FailureMessage = "Expected \"name\" to equal \"Root Catalog\", Actual: " + name; + } else { + Failure = false; + } +} + + + + false + + + + + + + + + + + continue + + false + ${loops} + + ${graphQLPoolUsers} + ${ramp_period} + 1505803944000 + 1505803944000 + false + + + mpaf/tool/fragments/_system/thread_group.jmx + + + 1 + false + 1 + ${graphqlGetListOfProductsByCategoryIdPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get List of Products by category_id"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + + true + + + + false + {"query":"query category($id: Int!, $currentPage: Int, $pageSize: Int) {\n category(id: $id) {\n product_count\n description\n url_key\n name\n id\n breadcrumbs {\n category_name\n category_url_key\n __typename\n }\n products(pageSize: $pageSize, currentPage: $currentPage) {\n total_count\n items {\n id\n name\n # small_image\n # short_description\n url_key\n special_price\n special_from_date\n special_to_date\n price {\n regularPrice {\n amount {\n value\n currency\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n","variables":{"id":${category_id},"currentPage":1,"pageSize":12},"operationName":"category"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_list_of_products_by_category_id.jmx + + + + + "name":"${category_name}","id":${category_id}, + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetSimpleProductDetailsByProductUrlKeyPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Simple Product Details by product_url_key"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_simple_product_details_by_product_url_key.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetSimpleProductDetailsByNamePercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Simple Product Details by name"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"query productDetail($name: String, $onServer: Boolean!) {\n productDetail: products(filter: { name: { eq: $name } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetail"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_simple_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetConfigurableProductDetailsByProductUrlKeyPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Configurable Product Detail by product_url_key"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + true + + + + false + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_product_url_key.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetConfigurableProductDetailsByNamePercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Configurable Product Detail by name"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + true + + + + false + {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetProductSearchByTextAndCategoryIdPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Product Search by text and category_id"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + + true + + + + false + {"query":"query productSearch($inputText: String!, $categoryId: String) {\n products(\n pageSize:12\n search: $inputText, filter: { category_id: { eq: $categoryId } }) {\n items {\n id\n name\n small_image {\n label\n url\n }\n url_key\n price {\n regularPrice {\n amount {\n value\n currency\n }\n }\n }\n }\n total_count\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n }\n }\n }\n}","variables":{"inputText":"Product","categoryId":"${category_id}"},"operationName":"productSearch"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_product_search_by_text_and_category_id.jmx + + + + graphql_search_products_query_total_count + $.data.products.total_count + + + BODY + + + + String totalCount=vars.get("graphql_search_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + + + false + + + + + + + + 1 + false + 1 + ${graphqlGetCategoryListByCategoryIdPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Category List by category_id"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + + true + + + + false + + {"query":"query categoryList($id: Int!) {\n category(id: $id) {\n id\n children {\n id\n name\n url_key\n url_path\n children_count\n path\n image\n productImagePreview: products(pageSize: 1) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"categoryList"} + + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_category_list_by_category_id.jmx + + + javascript + + + + var category = vars.getObject("category"); +var response = JSON.parse(prev.getResponseDataAsString()); + +assertCategoryId(category, response); +assertCategoryChildren(category, response); + +function assertCategoryId(category, response) { + if (response.data == undefined || response.data.category == undefined || response.data.category.id != category.id) { + AssertionResult.setFailureMessage("Cannot find category with id \"" + category.id + "\""); + AssertionResult.setFailure(true); + } +} + +function assertCategoryChildren(category, response) { + foundCategory = response.data && response.data.category ? response.data.category : null; + if (foundCategory) { + var childrenFound = foundCategory.children.map(function (c) {return parseInt(c.id)}); + var children = category.children.map(function (c) {return parseInt(c)}); + if (JSON.stringify(children.sort()) != JSON.stringify(childrenFound.sort())) { + AssertionResult.setFailureMessage("Cannot math children categories \"" + JSON.stringify(children) + "\" for to found one: \"" + JSON.stringify(childrenFound) + "\""); + AssertionResult.setFailure(true); + } + } + +} + + + + + + + + + + 1 + false + 1 + ${graphqlUrlInfoByUrlKeyPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Url Info by url_key"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + + true + + + + false + + {"query":"query resolveUrl($urlKey: String!) {\n urlResolver(url: $urlKey) {\n type\n id\n }\n}","variables":{"urlKey":"${category_url_key}${url_suffix}"},"operationName":"resolveUrl"} + + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_url_info_by_url_key.jmx + + + + {"type":"CATEGORY","id":${category_id}} + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetCmsPageByIdPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Cms Page by id"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var cmsPages = props.get("cms_pages"); +var number = random.nextInt(cmsPages.length); + +vars.put("cms_page_id", cmsPages[number].id); + + mpaf/tool/fragments/ce/setup/prepare_cms_page.jmx + + + + true + + + + false + + {"query":"query getCmsPage($id: Int!, $onServer: Boolean!) {\n cmsPage(id: $id) {\n url_key\n content\n content_heading\n title\n page_layout\n meta_title @include(if: $onServer)\n meta_keywords @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n}","variables":{"id":${cms_page_id},"onServer":false},"operationName":"getCmsPage"} + + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_get_cms_page_by_id.jmx + + + $.data.cmsPage.url_key + ${cms_page_id} + false + false + false + false + + + + + + + + 1 + false + 1 + ${graphqlGetNavigationMenuByCategoryIdPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Navigation Menu by category_id"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + + true + + + + false + {"query":"query navigationMenu($id: Int!) {\n category(id: $id) {\n id\n name\n product_count\n path\n children {\n id\n name\n position\n level\n url_key\n url_path\n product_count\n children_count\n path\n productImagePreview: products(pageSize: 1) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"navigationMenu"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_navigation_menu_by_category_id.jmx + + + + + "id":${category_id},"name":"${category_name}","product_count" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlCreateEmptyCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Create Empty Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlGetEmptyCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Get Empty Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_empty_cart.jmx + + + + + {"data":{"cart":{"items":[]}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlSetShippingAddressOnCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Set Shipping Address On Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_empty_cart.jmx + + + + + {"data":{"cart":{"items":[]}}} + + Assertion.response_data + false + 8 + + + + + + true + + + + false + {"query":"mutation {\n setShippingAddressesOnCart(\n input: {\n cart_id: \"${quote_id}\"\n shipping_addresses: [\n {\n address: {\n firstname: \"test firstname\"\n lastname: \"test lastname\"\n company: \"test company\"\n street: [\"test street 1\", \"test street 2\"]\n city: \"test city\"\n region: \"test region\"\n postcode: \"887766\"\n country_code: \"US\"\n telephone: \"88776655\"\n save_in_address_book: false\n }\n }\n ]\n }\n ) {\n cart {\n shipping_addresses {\n firstname\n lastname\n company\n street\n city\n postcode\n telephone\n country {\n code\n label\n }\n address_type\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/set_shipping_address_on_cart.jmx + + + + + {"data":{"setShippingAddressesOnCart":{"cart":{"shipping_addresses":[{"firstname":"test firstname","lastname":"test lastname","company":"test company","street":["test street 1","test street 2"],"city":"test city","postcode":"887766","telephone":"88776655","country":{"code":"US","label":"US"},"address_type":"SHIPPING"}]}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlSetBillingAddressOnCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Set Billing Address On Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_empty_cart.jmx + + + + + {"data":{"cart":{"items":[]}}} + + Assertion.response_data + false + 8 + + + + + + true + + + + false + {"query":"mutation {\n setBillingAddressOnCart(\n input: {\n cart_id: \"${quote_id}\"\n billing_address: {\n address: {\n firstname: \"test firstname\"\n lastname: \"test lastname\"\n company: \"test company\"\n street: [\"test street 1\", \"test street 2\"]\n city: \"test city\"\n region: \"test region\"\n postcode: \"887766\"\n country_code: \"US\"\n telephone: \"88776655\"\n save_in_address_book: false\n }\n }\n }\n ) {\n cart {\n billing_address {\n firstname\n lastname\n company\n street\n city\n postcode\n telephone\n country {\n code\n label\n }\n address_type\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/set_billing_address_on_cart.jmx + + + + + {"data":{"setBillingAddressOnCart":{"cart":{"billing_address":{"firstname":"test firstname","lastname":"test lastname","company":"test company","street":["test street 1","test street 2"],"city":"test city","postcode":"887766","telephone":"88776655","country":{"code":"US","label":"US"},"address_type":"BILLING"}}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlAddSimpleProductToCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Add Simple Product To Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"mutation { \n addSimpleProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n data: {\n qty: 2\n sku: \"${product_sku}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n qty\n product {\n sku\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_simple_product_to_cart.jmx + + + + + addSimpleProductsToCart + "sku":"${product_sku}" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlAddConfigurableProductToCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Add Configurable Product To Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + true + + + + false + {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + product_option + $.data.products.items[0].variants[0].product.sku + + + BODY + mpaf/tool/fragments/ce/graphql/extract_configurable_product_option.jmx + + + + + true + + + + false + {"query":"mutation {\n addConfigurableProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n variant_sku: \"${product_option}\"\n data: {\n qty: 2\n sku: \"${product_option}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n id\n qty\n product {\n name\n sku\n }\n ... on ConfigurableCartItem {\n configurable_options {\n option_label\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_configurable_product_to_cart.jmx + + + + + addConfigurableProductsToCart + "sku":"${product_option}" + + Assertion.response_data + false + 2 + + + + + + + + 1 + false + 1 + ${graphqlUpdateSimpleProductQtyInCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Update Simple Product Qty In Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"mutation { \n addSimpleProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n data: {\n qty: 2\n sku: \"${product_sku}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n qty\n product {\n sku\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_simple_product_to_cart.jmx + + + + + addSimpleProductsToCart + "sku":"${product_sku}" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_cart.jmx + + + + item_id + $.data.cart.items[0].id + + + BODY + + + + + {"data":{"cart":{"items": + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"mutation {\n updateCartItems(input: {\n cart_id: \"${quote_id}\"\n cart_items: [\n {\n cart_item_id: ${item_id}\n quantity: 5\n }\n ]\n }) {\n cart {\n items {\n id\n qty\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/update_simple_product_qty_in_cart.jmx + + + + + {"data":{"updateCartItems":{"cart":{"items":[{"id":"${item_id}","qty":5}]}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlUpdateConfigurableProductQtyInCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Update Configurable Product Qty In Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + true + + + + false + {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + product_option + $.data.products.items[0].variants[0].product.sku + + + BODY + mpaf/tool/fragments/ce/graphql/extract_configurable_product_option.jmx + + + + + true + + + + false + {"query":"mutation {\n addConfigurableProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n variant_sku: \"${product_option}\"\n data: {\n qty: 2\n sku: \"${product_option}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n id\n qty\n product {\n name\n sku\n }\n ... on ConfigurableCartItem {\n configurable_options {\n option_label\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_configurable_product_to_cart.jmx + + + + + addConfigurableProductsToCart + "sku":"${product_option}" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_cart.jmx + + + + item_id + $.data.cart.items[0].id + + + BODY + + + + + {"data":{"cart":{"items": + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"mutation {\n updateCartItems(input: {\n cart_id: \"${quote_id}\"\n cart_items: [\n {\n cart_item_id: ${item_id}\n quantity: 5\n }\n ]\n }) {\n cart {\n items {\n id\n qty\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/update_configurable_product_qty_in_cart.jmx + + + + + {"data":{"updateCartItems":{"cart":{"items":[{"id":"${item_id}","qty":5}]}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlRemoveSimpleProductFromCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Remove Simple Product From Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"mutation { \n addSimpleProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n data: {\n qty: 2\n sku: \"${product_sku}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n qty\n product {\n sku\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_simple_product_to_cart.jmx + + + + + addSimpleProductsToCart + "sku":"${product_sku}" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_cart.jmx + + + + item_id + $.data.cart.items[0].id + + + BODY + + + + + {"data":{"cart":{"items": + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"mutation {\n removeItemFromCart(\n input: {\n cart_id: \"${quote_id}\"\n cart_item_id: ${item_id}\n }\n ) {\n cart {\n items {\n qty\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/remove_simple_product_from_cart.jmx + + + + + {"data":{"removeItemFromCart":{"cart":{"items":[]}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlRemoveConfigurableProductFromCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Remove Configurable Product From Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + true + + + + false + {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + product_option + $.data.products.items[0].variants[0].product.sku + + + BODY + mpaf/tool/fragments/ce/graphql/extract_configurable_product_option.jmx + + + + + true + + + + false + {"query":"mutation {\n addConfigurableProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n variant_sku: \"${product_option}\"\n data: {\n qty: 2\n sku: \"${product_option}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n id\n qty\n product {\n name\n sku\n }\n ... on ConfigurableCartItem {\n configurable_options {\n option_label\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_configurable_product_to_cart.jmx + + + + + addConfigurableProductsToCart + "sku":"${product_option}" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_cart.jmx + + + + item_id + $.data.cart.items[0].id + + + BODY + + + + + {"data":{"cart":{"items": + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"mutation {\n removeItemFromCart(\n input: {\n cart_id: \"${quote_id}\"\n cart_item_id: ${item_id}\n }\n ) {\n cart {\n items {\n qty\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/remove_configurable_product_from_cart.jmx + + + + + {"data":{"removeItemFromCart":{"cart":{"items":[]}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlApplyCouponToCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Apply Coupon To Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"mutation { \n addSimpleProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n data: {\n qty: 2\n sku: \"${product_sku}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n qty\n product {\n sku\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_simple_product_to_cart.jmx + + + + + addSimpleProductsToCart + "sku":"${product_sku}" + + Assertion.response_data + false + 2 + + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var coupons = props.get("coupon_codes"); +number = random.nextInt(coupons.length); + +vars.put("coupon_code", coupons[number].code); + + mpaf/tool/fragments/ce/common/extract_coupon_code_setup.jmx + + + + + true + + + + false + {"query":"mutation {\n applyCouponToCart(input: {cart_id: \"${quote_id}\", coupon_code: \"${coupon_code}\"}) {\n cart {\n applied_coupon {\n code\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/apply_coupon_to_cart.jmx + + + + + {"data":{"applyCouponToCart":{"cart":{"applied_coupon":{"code":"${coupon_code}"}}}}} + + Assertion.response_data + false + 8 + + + + + + + + 1 + false + 1 + ${graphqlRemoveCouponFromCartPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Remove Coupon From Cart"); + + true + + + + + + + Content-Type + application/json + + + Accept + */* + + + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx + + + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + - - mpaf/tool/fragments/ce/api/create_grouped_product_with_extensible_data_objects.jmx - - - - true - - - - false - { - "product": { - "sku": "apsku-test-${__time()}-${__threadNum}-${__Random(1,1000000)}", - "name": "Extensible_Product_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "visibility": "4", - "type_id": "grouped", - "price": "3.62", - "status": "1", - "attribute_set_id": "4", - "custom_attributes": [ - { - "attribute_code": "cost", - "value": "" - }, - { - "attribute_code": "description", - "value": "Description" - } - ], - "extension_attributes":{ - "stock_item":{ - "manage_stock": 1, - "is_in_stock": 1, - "qty":"100" - } - } , - "media_gallery_entries": - [{ - "id": null, - "label":"test_label_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "position":1, - "disabled":false, - "media_type":"image", - "types":["image"], - "content":{ - "base64_encoded_data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iioLy8t9Ps5bu7lWKCIZd26KKaTbshpX0RPRXN/8J/4V/6DVv8Ak3+FH/Cf+Ff+g1b/AJN/hXR9SxP/AD7l9zNPYVf5X9x0lFc3/wAJ/wCFf+g1b/k3+FH/AAn/AIV/6DVv+Tf4UfUsT/z7l9zD2FX+V/cdJRXN/wDCf+Ff+g1b/k3+FH/Cf+Ff+g1b/k3+FH1LE/8APuX3MPYVf5X9x0lFc3/wn/hX/oNW/wCTf4Uf8J/4V/6DVv8Ak3+FH1LE/wDPuX3MPYVf5X9x0lFVdP1G01WyS8sZ1nt3JCyL0ODg/qKtVzyi4u0lZmbTTswrm/H3/Iiav/1x/wDZhXSVzfj7/kRNX/64/wDswrowf+80/wDEvzNKH8WPqj5voorB1zS7OLT7m7SHE5YNu3HqWGeM471+kYutOhSdSEU7Jt3dtF20f6H1FacqcHJK9vO36M3qKzTa6foqPdxwlWxswrFi2T0AJ9aRdVmjkT7XYSW8TsFEm8MAT0yB0qfrcafu1tJeV2l2u7K3zsL2yjpPR+V3+NjTorPn1GVbt7a1s2uJIwDJ84ULnpyaik1SWTTrp47Z0uIQRJGzAFOPvZ70Sx1GLau9L9H03SdrNrsgdeCuu3k+hq0VR0ma4msImuIih2LtYvuLjA+b2zV6uijUVWmprqaQkpxUl1PoP4Xf8iBYf78v/oxq7GuO+F3/ACIFh/vy/wDoxq7GvzTMf98q/wCJ/mfLYn+NP1YVzfj7/kRNX/64/wDswrpK5vx9/wAiJq//AFx/9mFRg/8Aeaf+JfmTQ/ix9UfN9ZniD/kB3H/Af/QhWnTZI45kKSIroeqsMg1+l4mk61GdNfaTX3o+pqw54Sj3Rma/GXsI3BcLFMruU+8F5yR+dUZ4tOeNFOq3tx5jACNZg5J+mK6PrUMdrbxPvjgiR/7yoAa48TgPa1HNW1STvfp2s1+JjVw/PJy017mbe/YTqTB7iWzuQgPmhtocfjwajiupbjTtTieUXCxRsqTKMb8qePwrYlghnAE0UcgHQOoP86ckaRoERFVR/CowKbwU3UclJJO+19brqr203vvoHsJczd7J3/H8PmVNJnhm063WOVHZIkDhTkqcd/yNXajighg3eTFHHu67FAz+VSV2UIShTjGe67G9NOMUpbn0H8Lv+RAsP9+X/wBGNXY1x3wu/wCRAsP9+X/0Y1djX5tmP++Vf8T/ADPl8T/Gn6sK5vx9/wAiJq//AFx/9mFdJXN+Pv8AkRNX/wCuP/swqMH/ALzT/wAS/Mmh/Fj6o+b6KKK/Uj60KKKKACiiigAooooA+g/hd/yIFh/vy/8Aoxq7GuO+F3/IgWH+/L/6Mauxr8wzH/fKv+J/mfKYn+NP1YVzfj7/AJETV/8Arj/7MK6Sub8ff8iJq/8A1x/9mFRg/wDeaf8AiX5k0P4sfVHzfRRRX6kfWhRRRQAUUUUAFFFFAH0H8Lv+RAsP9+X/ANGNXY1x3wu/5ECw/wB+X/0Y1djX5hmP++Vf8T/M+UxP8afqwqC8s7fULOW0u4llglGHRujCp6K5E2ndGKdtUc3/AMIB4V/6Atv+bf40f8IB4V/6Atv+bf410lFdH13E/wDPyX3s09vV/mf3nN/8IB4V/wCgLb/m3+NH/CAeFf8AoC2/5t/jXSUUfXcT/wA/Jfew9vV/mf3nN/8ACAeFf+gLb/m3+NH/AAgHhX/oC2/5t/jXSUUfXcT/AM/Jfew9vV/mf3nN/wDCAeFf+gLb/m3+NH/CAeFf+gLb/m3+NdJRR9dxP/PyX3sPb1f5n95V0/TrTSrJLOxgWC3QkrGvQZOT+pq1RRXPKTk7yd2Zttu7P//Z", - "type": "image/jpeg", - "name": "test_image_${__time(YMDHMS)}-${__threadNum}-${__Random(1,1000000)}.jpeg" - } - } - ] - } - } - = - - - - - - - - ${request_protocol} - - ${base_path}rest/default/V1/products - POST - true - false - true - false - false - - - - - grouped_product_id - $.id - - - BODY - - - - grouped_product_sku - $.sku - - - BODY - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - grouped_product_id - - - - - ^[a-z0-9-]+$ - - Assertion.response_data - false - 1 - variable - grouped_product_sku - - - - - - - true - - - - false - { - "items": [ - { - "sku": "${grouped_product_sku}", - "link_type": "associated", - "linked_product_sku": "${bundle_product_sku}", - "linked_product_type": "bundle", - "position": 1, - "extension_attributes": { - "qty": 1 - } - }, - { - "sku": "${grouped_product_sku}", - "link_type": "associated", - "linked_product_sku": "${configurable_product_sku}", - "linked_product_type": "configurable", - "position": 2, - "extension_attributes": { - "qty": 1 - } - }, - { - "sku": "${grouped_product_sku}", - "link_type": "associated", - "linked_product_sku": "${simple_product_sku}", - "linked_product_type": "simple", - "position": 3, - "extension_attributes": { - "qty": 1 - } - }, - { - "sku": "${grouped_product_sku}", - "link_type": "associated", - "linked_product_sku": "${downloadable_product_sku}", - "linked_product_type": "downloadable", - "position": 4, - "extension_attributes": { - "qty": 1 - } - } - ] - } - - = - - - - - - - - ${request_protocol} - - ${base_path}rest/default/V1/products/${grouped_product_sku}/links - POST - true - false - true - false - false - - - - - grouped_product_response - $ - - - BODY - - - - - true - - Assertion.response_data - false - 8 - variable - grouped_product_response - - - - - + + true + + + + false + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx + + + + quote_id + $.data.createEmptyCart + + + BODY + + + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"mutation { \n addSimpleProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n data: {\n qty: 2\n sku: \"${product_sku}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n qty\n product {\n sku\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/add_simple_product_to_cart.jmx + + + + + addSimpleProductsToCart + "sku":"${product_sku}" + + Assertion.response_data + false + 2 + + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var coupons = props.get("coupon_codes"); +number = random.nextInt(coupons.length); + +vars.put("coupon_code", coupons[number].code); + + mpaf/tool/fragments/ce/common/extract_coupon_code_setup.jmx + + + + + true + + + + false + {"query":"mutation {\n applyCouponToCart(input: {cart_id: \"${quote_id}\", coupon_code: \"${coupon_code}\"}) {\n cart {\n applied_coupon {\n code\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/apply_coupon_to_cart.jmx + + + + + {"data":{"applyCouponToCart":{"cart":{"applied_coupon":{"code":"${coupon_code}"}}}}} + + Assertion.response_data + false + 8 + + + + + + true + + + + false + {"query":"mutation {\n removeCouponFromCart(input: {cart_id: \"${quote_id}\"}) {\n cart {\n applied_coupon {\n code\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/remove_coupon_from_cart.jmx + + + + + {"data":{"removeCouponFromCart":{"cart":{"applied_coupon":null}}}} + + Assertion.response_data + false + 8 + + + - - mpaf/tool/fragments/ce/simple_controller.jmx - + + 1 + false + 1 + ${graphqlCatalogBrowsingByGuestPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Catalog Browsing By Guest"); + + true + + + @@ -40732,24 +43269,60 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu mpaf/tool/fragments/ce/api/header_manager_before_token.jmx - + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + + + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + + mpaf/tool/fragments/ce/common/extract_category_setup.jmx + + + true false - {"username":"${admin_user}","password":"${admin_password}"} + {"query":"query navigationMenu($id: Int!) {\n category(id: $id) {\n id\n name\n product_count\n path\n children {\n id\n name\n position\n level\n url_key\n url_path\n product_count\n children_count\n path\n productImagePreview: products(pageSize: 1) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}","variables":{"id":${category_id}},"operationName":"navigationMenu"} = - + ${graphql_port_number} 60000 200000 ${request_protocol} - ${base_path}rest/V1/integration/admin/token + ${base_path}graphql POST true false @@ -40757,111 +43330,490 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu false false - mpaf/tool/fragments/ce/api/admin_token_retrieval.jmx + mpaf/tool/fragments/ce/graphql/get_navigation_menu_by_category_id.jmx + - - admin_token - $ + + + "id":${category_id},"name":"${category_name}","product_count" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"query productSearch($inputText: String!, $categoryId: String) {\n products(\n pageSize:12\n search: $inputText, filter: { category_id: { eq: $categoryId } }) {\n items {\n id\n name\n small_image {\n label\n url\n }\n url_key\n price {\n regularPrice {\n amount {\n value\n currency\n }\n }\n }\n }\n total_count\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n }\n }\n }\n}","variables":{"inputText":"Product","categoryId":"${category_id}"},"operationName":"productSearch"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_product_search_by_text_and_category_id.jmx + + + + graphql_search_products_query_total_count + $.data.products.total_count BODY - + + String totalCount=vars.get("graphql_search_products_query_total_count"); + +if (totalCount == null) { + Failure = true; + FailureMessage = "Not Expected \"totalCount\" to be null"; +} else { + if (Integer.parseInt(totalCount) < 1) { + Failure = true; + FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; + } else { + Failure = false; + } +} + + + + + + false + + + + + + true + + + + false + + {"query":"query resolveUrl($urlKey: String!) {\n urlResolver(url: $urlKey) {\n type\n id\n }\n}","variables":{"urlKey":"${category_url_key}${url_suffix}"},"operationName":"resolveUrl"} + + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_url_info_by_url_key.jmx + + - ^[a-z0-9-]+$ + {"type":"CATEGORY","id":${category_id}} Assertion.response_data false - 1 - variable - admin_token + 2 + + + + + + true + + + + false + {"query":"query category($id: Int!, $currentPage: Int, $pageSize: Int) {\n category(id: $id) {\n product_count\n description\n url_key\n name\n id\n breadcrumbs {\n category_name\n category_url_key\n __typename\n }\n products(pageSize: $pageSize, currentPage: $currentPage) {\n total_count\n items {\n id\n name\n # small_image\n # short_description\n url_key\n special_price\n special_from_date\n special_to_date\n price {\n regularPrice {\n amount {\n value\n currency\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n","variables":{"id":${category_id},"currentPage":1,"pageSize":12},"operationName":"category"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_list_of_products_by_category_id.jmx + + + + + "name":"${category_name}","id":${category_id}, + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + + + + true + + + + false + {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_product_url_key.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + +import java.util.Random; + +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); + +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + + + + true + + + + false + {"query":"query productDetail($name: String, $onServer: Boolean!) {\n productDetail: products(filter: { name: { eq: $name } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetail"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_simple_product_details_by_name.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + + + + + true + + + + false + {"query":"query productDetail($urlKey: String, $onServer: Boolean!) {\n productDetail: products(filter: { url_key: { eq: $urlKey } }) {\n items {\n sku\n name\n price {\n regularPrice {\n amount {\n currency\n value\n }\n }\n }\n description {html}\n media_gallery_entries {\n label\n position\n disabled\n file\n }\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n # Yes, Products have `meta_keyword` and\n # everything else has `meta_keywords`.\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"urlKey":"${product_url_key}","onServer":false},"operationName":"productDetail"} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_simple_product_details_by_product_url_key.jmx + + + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + + javascript + + + + random = vars.getObject("randomIntGenerator"); + +var cmsPages = props.get("cms_pages"); +var number = random.nextInt(cmsPages.length); + +vars.put("cms_page_id", cmsPages[number].id); + + mpaf/tool/fragments/ce/setup/prepare_cms_page.jmx + + + + true + + + + false + + {"query":"query getCmsPage($id: Int!, $onServer: Boolean!) {\n cmsPage(id: $id) {\n url_key\n content\n content_heading\n title\n page_layout\n meta_title @include(if: $onServer)\n meta_keywords @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n}","variables":{"id":${cms_page_id},"onServer":false},"operationName":"getCmsPage"} + + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_get_cms_page_by_id.jmx + + + $.data.cmsPage.url_key + ${cms_page_id} + false + false + false + false + + + + + + + + 1 + false + 1 + ${graphqlCheckoutByGuestPercentage} + mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx + + + +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + + javascript + mpaf/tool/fragments/_system/setup_label.jmx + + + + vars.put("testLabel", "GraphQL Checkout By Guest"); + + true + + + - Authorization - Bearer ${admin_token} + Content-Type + application/json + + + Accept + */* - mpaf/tool/fragments/ce/api/header_manager.jmx + mpaf/tool/fragments/ce/api/header_manager_before_token.jmx - - true - - - - false - {"query":"{\n products(\n filter: {\n price: {gt: \"10\"}\n or: {\n sku:{like:\"%Product%\"}\n name:{like:\"%Configurable Product%\"}\n }\n }\n pageSize: 200\n currentPage: 1\n sort: {\n price: ASC\n name:DESC\n }\n ) {\n total_count\n items {\n attribute_set_id\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n \t... on PhysicalProductInterface {\n \tweight\n \t}\n }\n page_info {\n page_size\n current_page\n }\n }\n}\n","variables":null,"operationName":null} - = - - - - - - - - ${request_protocol} - - ${base_path}graphql - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_filter_only.jmx - - - - graphql_multiple_products_query_total_count - $.data.products.total_count - - - BODY - - - - String totalCount=vars.get("graphql_multiple_products_query_total_count"); - if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; - } else { - if (Integer.parseInt(totalCount) < 200) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be greater than 200, Actual: " + totalCount; - } else { - Failure = false; - } - } - - - - false - - - + + mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx + +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + + + + true + + - + true false - {"query":"{\n products(filter: {sku: { eq: \"${simple_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"mutation {\n createEmptyCart\n}","variables":null,"operationName":null} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -40872,72 +43824,43 @@ vars.put("configurable_sku", "Configurable Product - ${__time(YMD)}-${__threadNu false false - mpaf/tool/fragments/ce/graphql/query_simple_product_with_extensible_data_objects.jmx + mpaf/tool/fragments/ce/graphql/create_empty_cart.jmx - - graphql_simple_products_query_total_count - $.data.products.total_count + + quote_id + $.data.createEmptyCart BODY - - - graphql_multiple_products_query_response - $ - - - BODY - - - - String totalCount=vars.get("graphql_simple_products_query_total_count"); - -if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; -} else { - if (Integer.parseInt(totalCount) != 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; - } else { - Failure = false; - } -} - - - - false - - - - $.data.products.items[0].sku - ${simple_product_sku} - true - false - false - true - + + + {"data":{"createEmptyCart":" + + Assertion.response_data + false + 2 + - + true false - {"query":"{\n products(filter: {sku: {eq:\"${configurable_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n items {\n id\n qty\n product {\n sku\n }\n }\n }\n}","variables":null,"operationName":null} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -40948,65 +43871,55 @@ if (totalCount == null) { false false - mpaf/tool/fragments/ce/graphql/query_configurable_product_with_extensible_data_objects.jmx + mpaf/tool/fragments/ce/graphql/get_empty_cart.jmx - - graphql_configurable_products_query_total_count - $.data.products.total_count - - - BODY - + + + {"data":{"cart":{"items":[]}}} + + Assertion.response_data + false + 8 + - - String totalCount=vars.get("graphql_configurable_products_query_total_count"); - -if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; -} else { - if (Integer.parseInt(totalCount) != 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; - } else { - Failure = false; - } -} + + + + +import java.util.Random; +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("configurable_products_list").size()); +product = props.get("configurable_products_list").get(number); - - - - false - - - - $.data.products.items[0].sku - ${configurable_product_sku} - true - false - false - true - - - +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/configurable_products_setup.jmx + - + true false - {"query":"{\n products(\n search: \"configurable\"\n filter: {price: {gteq: \"1\"} }\n ) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"query productDetailByName($name: String, $onServer: Boolean!) {\n products(filter: { name: { eq: $name } }) {\n items {\n id\n sku\n name\n ... on ConfigurableProduct {\n configurable_options {\n attribute_code\n attribute_id\n id\n label\n values {\n default_label\n label\n store_label\n use_default_value\n value_index\n }\n }\n variants {\n product {\n #fashion_color\n #fashion_size\n id\n media_gallery_entries {\n disabled\n file\n label\n position\n }\n sku\n stock_status\n }\n }\n }\n meta_title @include(if: $onServer)\n meta_keyword @include(if: $onServer)\n meta_description @include(if: $onServer)\n }\n }\n}","variables":{"name":"${product_name}","onServer":false},"operationName":"productDetailByName"} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -41017,56 +43930,44 @@ if (totalCount == null) { false false - mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_and_filter.jmx + mpaf/tool/fragments/ce/graphql/get_configurable_product_details_by_name.jmx - - graphql_search_products_query_total_count_fulltext_filter - $.data.products.total_count - - - BODY - - - - String totalCount=vars.get("graphql_search_products_query_total_count_fulltext_filter"); - -if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; -} else { - if (Integer.parseInt(totalCount) < 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; - } else { - Failure = false; - } -} - - - - - - false - + + + "sku":"${product_sku}","name":"${product_name}" + + Assertion.response_data + false + 2 + - + + + product_option + $.data.products.items[0].variants[0].product.sku + + + BODY + mpaf/tool/fragments/ce/graphql/extract_configurable_product_option.jmx + + - + true false - {"query":"{\n products(search: \"configurable\") {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t\t}\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"mutation {\n addConfigurableProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n variant_sku: \"${product_option}\"\n data: {\n qty: 2\n sku: \"${product_option}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n id\n qty\n product {\n name\n sku\n }\n ... on ConfigurableCartItem {\n configurable_options {\n option_label\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -41077,56 +43978,56 @@ if (totalCount == null) { false false - mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_only.jmx + mpaf/tool/fragments/ce/graphql/add_configurable_product_to_cart.jmx - - graphql_search_products_query_total_count - $.data.products.total_count - - - BODY - + + + addConfigurableProductsToCart + "sku":"${product_option}" + + Assertion.response_data + false + 2 + - - String totalCount=vars.get("graphql_search_products_query_total_count"); - -if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; -} else { - if (Integer.parseInt(totalCount) < 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; - } else { - Failure = false; - } -} + + + + +import java.util.Random; +Random random = vars.getObject("randomIntGenerator"); +number = random.nextInt(props.get("simple_products_list").size()); +product = props.get("simple_products_list").get(number); - - - - false - - - +vars.put("product_url_key", product.get("url_key")); +vars.put("product_id", product.get("id")); +vars.put("product_name", product.get("title")); +vars.put("product_uenc", product.get("uenc")); +vars.put("product_sku", product.get("sku")); + + + + true + mpaf/tool/fragments/ce/product_browsing_and_adding_items_to_the_cart/simple_products_setup.jmx + - + true false - {"query":"{\n products(search: \"Option 1\") {\n filters {\n name\n filter_items_count\n request_var\n filter_items {\n label\n value_string\n items_count\n ... on SwatchLayerFilterItemInterface {\n swatch_data {\n type\n value\n }\n }\n }\n }\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n weight\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_id\n label\n position\n use_default\n attribute_code\n values {\n value_index\n label\n store_label\n default_label\n use_default_value\n }\n product_id\n }\n variants {\n product {\n ... on PhysicalProductInterface {\n weight\n }\n sku\n color\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n\n\n }\n attributes {\n label\n code\n value_index\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + {"query":"mutation { \n addSimpleProductsToCart(\n input: {\n cart_id: \"${quote_id}\"\n cartItems: [\n {\n data: {\n qty: 2\n sku: \"${product_sku}\"\n }\n }\n ]\n }\n ) {\n cart {\n items {\n qty\n product {\n sku\n }\n }\n }\n }\n}","variables":null,"operationName":null} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -41137,212 +44038,75 @@ if (totalCount == null) { false false - mpaf/tool/fragments/ce/graphql/query_multiple_products_with_extensible_data_objects_using_full_text_and_filters.jmx + mpaf/tool/fragments/ce/graphql/add_simple_product_to_cart.jmx - - graphql_search_products_query_total_count - $.data.products.total_count - - - BODY - - - - String totalCount=vars.get("graphql_search_products_query_total_count"); - -if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; -} else { - if (Integer.parseInt(totalCount) < 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be greater than zero, Actual: " + totalCount; - } else { - Failure = false; - } -} - - - - - - false - + + + addSimpleProductsToCart + "sku":"${product_sku}" + + Assertion.response_data + false + 2 + - - true - - - - false - {"query":"{\nproducts(filter: {sku: {eq:\"${bundle_product_sku}\"} }) {\n total_count\n items {\n ... on PhysicalProductInterface {\n weight\n }\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on BundleProduct {\n weight\n price_view\n dynamic_price\n dynamic_sku\n ship_bundle_items\n dynamic_weight\n items {\n option_id\n title\n required\n type\n position\n sku\n options {\n id\n qty\n position\n is_default\n price\n price_type\n can_change_quantity\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} - = - - - - - - - - ${request_protocol} - - ${base_path}graphql - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/graphql/query_bundle_product_with_extensible_data_objects.jmx - - - - graphql_bundle_products_query_total_count - $.data.products.total_count - - - BODY - - - - String totalCount=vars.get("graphql_bundle_products_query_total_count"); - - if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; - } else { - if (Integer.parseInt(totalCount) != 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; - } else { - Failure = false; - } - } - - - - - - false - - - - $.data.products.items[0].sku - ${bundle_product_sku} - true - false - false - false - - - - - - true - - - - false - {"query":"{\n products(filter: {sku: { eq: \"${downloadable_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n ... on DownloadableProduct {\n links_purchased_separately\n links_title\n downloadable_product_samples {\n id\n title\n sort_order\n sample_type\n sample_file\n sample_url\n }\n downloadable_product_links {\n id\n title\n sort_order\n is_shareable\n price\n number_of_downloads\n link_type\n sample_type\n sample_file\n sample_url\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} - = - - - - - - - - ${request_protocol} - - ${base_path}graphql - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/graphql/query_downloadable_product_with_extensible_data_objects.jmx - - - - graphql_downloadable_products_query_total_count - $.data.products.total_count - - - BODY - - - - String totalCount=vars.get("graphql_downloadable_products_query_total_count"); - - if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; - } else { - if (Integer.parseInt(totalCount) != 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; - } else { - Failure = false; - } - } - - - - - - false - - - - $.data.products.items[0].sku - ${downloadable_product_sku} - true - false - false - true - - - - $.data.products.items[0].downloadable_product_samples..title - ["sample1","sample2"] - true - false - false - false - - - - $.data.products.items[0].downloadable_product_samples..title - ["sample1","sample2"] - true - false - false - false - - - - - + + true + + + + false + {"query":"mutation {\n setBillingAddressOnCart(\n input: {\n cart_id: \"${quote_id}\"\n billing_address: {\n address: {\n firstname: \"test firstname\"\n lastname: \"test lastname\"\n company: \"test company\"\n street: [\"test street 1\", \"test street 2\"]\n city: \"test city\"\n region: \"test region\"\n postcode: \"887766\"\n country_code: \"US\"\n telephone: \"88776655\"\n save_in_address_book: false\n }\n }\n }\n ) {\n cart {\n billing_address {\n firstname\n lastname\n company\n street\n city\n postcode\n telephone\n country {\n code\n label\n }\n address_type\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/set_billing_address_on_cart.jmx + + + + + {"data":{"setBillingAddressOnCart":{"cart":{"billing_address":{"firstname":"test firstname","lastname":"test lastname","company":"test company","street":["test street 1","test street 2"],"city":"test city","postcode":"887766","telephone":"88776655","country":{"code":"US","label":"US"},"address_type":"BILLING"}}}}} + + Assertion.response_data + false + 8 + + + + + true false - {"query":"{\n products(filter: {sku: { eq: \"${virtual_product_sku}\" } })\n {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on PhysicalProductInterface {\n \t\t\tweight\n \t\t }\n }\n }\n}\n","variables":null,"operationName":null} + {"query":"mutation {\n setShippingAddressesOnCart(\n input: {\n cart_id: \"${quote_id}\"\n shipping_addresses: [\n {\n address: {\n firstname: \"test firstname\"\n lastname: \"test lastname\"\n company: \"test company\"\n street: [\"test street 1\", \"test street 2\"]\n city: \"test city\"\n region: \"test region\"\n postcode: \"887766\"\n country_code: \"US\"\n telephone: \"88776655\"\n save_in_address_book: false\n }\n }\n ]\n }\n ) {\n cart {\n shipping_addresses {\n firstname\n lastname\n company\n street\n city\n postcode\n telephone\n country {\n code\n label\n }\n address_type\n }\n }\n }\n}","variables":null,"operationName":null} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -41353,321 +44117,167 @@ if (totalCount == null) { false false - mpaf/tool/fragments/ce/graphql/query_virtual_product_with_extensible_data_objects.jmx + mpaf/tool/fragments/ce/graphql/set_shipping_address_on_cart.jmx - - graphql_virtual_products_query_total_count - $.data.products.total_count + + + {"data":{"setShippingAddressesOnCart":{"cart":{"shipping_addresses":[{"firstname":"test firstname","lastname":"test lastname","company":"test company","street":["test street 1","test street 2"],"city":"test city","postcode":"887766","telephone":"88776655","country":{"code":"US","label":"US"},"address_type":"SHIPPING"}]}}}} + + Assertion.response_data + false + 8 + + + + + + true + + + + false + {"query":"mutation {\n setPaymentMethodOnCart(input: {\n cart_id: \"${quote_id}\", \n payment_method: {\n code: \"checkmo\"\n }\n }) {\n cart {\n selected_payment_method {\n code\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/set_payment_method_on_cart.jmx + + + + + {"data":{"setPaymentMethodOnCart":{"cart":{"selected_payment_method":{"code":"checkmo"}}}}} + + Assertion.response_data + false + 8 + + + + + + true + + + + false + {"query":"{\n cart(cart_id: \"${quote_id}\") {\n shipping_addresses {\n address_id\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/get_current_shipping_address.jmx + + + + address_id + $.data.cart.shipping_addresses[0].address_id BODY - - String totalCount=vars.get("graphql_virtual_products_query_total_count"); - -if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; -} else { - if (Integer.parseInt(totalCount) != 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; - } else { - Failure = false; - } -} - - - - false - - - - $.data.products.items[0].sku - ${virtual_product_sku} - true - false - false - true - + + + + true + + + + false + {"query":"mutation {\n setShippingMethodsOnCart(input: \n {\n cart_id: \"${quote_id}\", \n shipping_methods: [{\n cart_address_id: ${address_id}\n carrier_code: \"flatrate\"\n method_code: \"flatrate\"\n }]\n }) {\n cart {\n shipping_addresses {\n selected_shipping_method {\n carrier_code\n method_code\n }\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/set_shipping_method_on_cart.jmx + + + + + {"data":{"setShippingMethodsOnCart":{"cart":{"shipping_addresses":[{"selected_shipping_method":{"carrier_code":"flatrate","method_code":"flatrate"}}]}}}} + + Assertion.response_data + false + 8 + - - true - - - - false - {"query":"{\nproducts(filter: {sku: {eq:\"${grouped_product_sku}\"} }) {\n total_count\n items {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n ... on GroupedProduct {\n weight\n items {\n qty\n position\n product {\n attribute_set_id\n categories\n {\n id\n position\n }\n country_of_manufacture\n created_at\n description {\n html\n }\n gift_message_available\n id\n image\n {\n url\n label\n }\n meta_description\n meta_keyword\n meta_title\n media_gallery_entries\n {\n disabled\n file\n id\n label\n media_type\n position\n types\n content\n {\n base64_encoded_data\n type\n name\n }\n video_content\n {\n media_type\n video_description\n video_metadata\n video_provider\n video_title\n video_url\n }\n }\n name\n new_from_date\n new_to_date\n options_container\n ... on CustomizableProductInterface {\n options\n {\n title\n required\n sort_order\n }\n }\n \n price {\n minimalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n maximalPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n regularPrice {\n amount {\n value\n currency\n }\n adjustments {\n amount {\n value\n currency\n }\n code\n description\n }\n }\n }\n product_links\n {\n link_type\n linked_product_sku\n linked_product_type\n position\n sku\n }\n short_description {\n html\n }\n sku\n small_image\n {\n url\n label\n }\n special_from_date\n special_price\n special_to_date\n swatch_image\n thumbnail\n {\n url\n label\n }\n tier_price\n tier_prices\n {\n customer_group_id\n percentage_value\n qty\n value\n website_id\n }\n type_id\n updated_at\n url_key\n url_path\n websites { id name code sort_order default_group_id is_default }\n }\n }\n }\n }\n }\n}\n","variables":null,"operationName":null} - = - - - - - - - - ${request_protocol} - - ${base_path}graphql - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/graphql/query_grouped_product_with_extensible_data_objects.jmx - - - - graphql_grouped_products_query_total_count - $.data.products.total_count - - - BODY - - - - String totalCount=vars.get("graphql_grouped_products_query_total_count"); - - if (totalCount == null) { - Failure = true; - FailureMessage = "Not Expected \"totalCount\" to be null"; - } else { - if (Integer.parseInt(totalCount) != 1) { - Failure = true; - FailureMessage = "Expected \"totalCount\" to be equal to 1, Actual: " + totalCount; - } else { - Failure = false; - } - } - - - - - - false - - - - $.data.products.items[0].sku - ${grouped_product_sku} - true - false - false - true - - - - - - - - true - - - - false - { - "customer": { - - "email": "customer_${__time()}-${__threadNum}-${__Random(1,1000000)}@example.com", - "firstname": "test_${__time()}-${__threadNum}-${__Random(1,1000000)}", - "lastname": "Doe" - }, - "password": "test@123" - } - = - - - - - - - - ${request_protocol} - - ${base_path}rest/default/V1/customers - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/graphql/query_frontend_customer.jmx - - - - customer_id - $.id - - - BODY - - - - - ^\d+$ - - Assertion.response_data - false - 1 - variable - customer_id - - - - - - - - - - - - ${request_protocol} - - ${base_path}rest/default/V1/customers/${customer_id} - GET - true - false - true - false - false - - mpaf/tool/fragments/ce/api/check_customer.jmx - - - - $.id - ${customer_id} - true - false - false - true - - - - customer_email - $.email - - - BODY - - - - - true - - - - false - { - "username":"${customer_email}", - "password":"test@123" - } - - = - - - - - - - - ${request_protocol} - - ${base_path}rest/default/V1/integration/customer/token - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/api/create_customer.jmx - - - - customer_token - $ - - - BODY - - - - - true - - - - false - {"query":"{\n customer {\n created_at\n group_id\n\n prefix\n firstname\n middlename\n lastname\n suffix\n email\n default_billing\n default_shipping\n\n dob\n taxvat\n\n id\n addresses {\n id\n customer_id\n region {\n region_code\n region\n region_id\n }\n region_id\n country_id\n street \n company\n telephone\n fax\n postcode\n city\n firstname\n lastname\n middlename\n prefix\n suffix\n vat_id\n default_shipping\n default_billing\n }\n is_subscribed\n }\n}","variables":null,"operationName":null} - = - - - - - - - - ${request_protocol} - - ${base_path}graphql - POST - true - false - true - false - false - - mpaf/tool/fragments/ce/graphql/query_frontend_customer.jmx - - - - false - - - import org.apache.jmeter.protocol.http.control.Header; + + javascript + + + + random = vars.getObject("randomIntGenerator"); - sampler.getHeaderManager().removeHeaderNamed("Authorization"); +var coupons = props.get("coupon_codes"); +number = random.nextInt(coupons.length); - sampler.getHeaderManager().add(new Header("Authorization","Bearer " + vars.get("customer_token"))); - - - - $.data.customer.lastname - Doe - true - false - false - true - - - - - - +vars.put("coupon_code", coupons[number].code); + + mpaf/tool/fragments/ce/common/extract_coupon_code_setup.jmx + + + + true false - {"query":"{\n category(id: 1) {\n name\n id\n level\n description\n path\n path_in_store\n url_key\n url_path\n children {\n id\n description\n default_sort_by\n children {\n id\n description\n level\n children {\n level\n id\n children {\n id\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null} + {"query":"mutation {\n applyCouponToCart(input: {cart_id: \"${quote_id}\", coupon_code: \"${coupon_code}\"}) {\n cart {\n applied_coupon {\n code\n }\n }\n }\n}","variables":null,"operationName":null} = - - - + ${graphql_port_number} + 60000 + 200000 ${request_protocol} ${base_path}graphql @@ -41678,39 +44288,59 @@ if (totalCount == null) { false false - mpaf/tool/fragments/ce/graphql/query_root_category.jmx + mpaf/tool/fragments/ce/graphql/apply_coupon_to_cart.jmx - - graphql_category_query_name - $.data.category.name - - - BODY - + + + {"data":{"applyCouponToCart":{"cart":{"applied_coupon":{"code":"${coupon_code}"}}}}} + + Assertion.response_data + false + 8 + - - String name = vars.get("graphql_category_query_name"); -if (name == null) { - Failure = true; - FailureMessage = "Not Expected \"children\" to be null"; -} else { - if (!name.equals("Root Catalog")) { - Failure = true; - FailureMessage = "Expected \"name\" to equal \"Root Catalog\", Actual: " + name; - } else { - Failure = false; - } -} - - - - false - + + + + true + + + + false + {"query":"mutation {\n removeCouponFromCart(input: {cart_id: \"${quote_id}\"}) {\n cart {\n applied_coupon {\n code\n }\n }\n }\n}","variables":null,"operationName":null} + = + + + + + ${graphql_port_number} + 60000 + 200000 + ${request_protocol} + + ${base_path}graphql + POST + true + false + true + false + false + + mpaf/tool/fragments/ce/graphql/remove_coupon_from_cart.jmx + + + + + {"data":{"removeCouponFromCart":{"cart":{"applied_coupon":null}}}} + + Assertion.response_data + false + 8 + - @@ -41846,6 +44476,14 @@ if (name == null) { ${response_time_file_name} mpaf/tool/fragments/ce/common/aggregate_graph.jmx + + + false + true + true + false + mpaf/tool/fragments/_system/debug.jmx + true diff --git a/setup/performance-toolkit/profiles/ce/extra_large.xml b/setup/performance-toolkit/profiles/ce/extra_large.xml index 390bf7fb12003..911ac7fe06d3b 100644 --- a/setup/performance-toolkit/profiles/ce/extra_large.xml +++ b/setup/performance-toolkit/profiles/ce/extra_large.xml @@ -39,6 +39,7 @@ 20 20 2 + 20 100 30 diff --git a/setup/performance-toolkit/profiles/ce/large.xml b/setup/performance-toolkit/profiles/ce/large.xml index ed91b22930af5..79abab0ba4b95 100644 --- a/setup/performance-toolkit/profiles/ce/large.xml +++ b/setup/performance-toolkit/profiles/ce/large.xml @@ -39,6 +39,7 @@ 20 20 2 + 20 50 20 diff --git a/setup/performance-toolkit/profiles/ce/medium.xml b/setup/performance-toolkit/profiles/ce/medium.xml index f01eabb7898f3..d02370a7770b3 100644 --- a/setup/performance-toolkit/profiles/ce/medium.xml +++ b/setup/performance-toolkit/profiles/ce/medium.xml @@ -39,6 +39,7 @@ 20 20 2 + 20 30 10 diff --git a/setup/performance-toolkit/profiles/ce/medium_msite.xml b/setup/performance-toolkit/profiles/ce/medium_msite.xml index a57fcad0779fe..2f9310c5242cb 100644 --- a/setup/performance-toolkit/profiles/ce/medium_msite.xml +++ b/setup/performance-toolkit/profiles/ce/medium_msite.xml @@ -45,6 +45,7 @@ 20 20 2 + 20 30 10 diff --git a/setup/performance-toolkit/profiles/ce/small.xml b/setup/performance-toolkit/profiles/ce/small.xml index 60ae901d8f5e0..cf7768328bd69 100644 --- a/setup/performance-toolkit/profiles/ce/small.xml +++ b/setup/performance-toolkit/profiles/ce/small.xml @@ -39,6 +39,7 @@ 20 20 2 + 20 10 5 diff --git a/setup/pub/styles/setup.css b/setup/pub/styles/setup.css index 13dc7b2a043d2..fa7b2e1c51d3c 100644 --- a/setup/pub/styles/setup.css +++ b/setup/pub/styles/setup.css @@ -3,4 +3,4 @@ * See COPYING.txt for license details. */ -.abs-action-delete,.abs-icon,.action-close:before,.action-next:before,.action-previous:before,.admin-user .admin__action-dropdown:before,.admin__action-multiselect-dropdown:before,.admin__action-multiselect-search-label:before,.admin__control-checkbox+label:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:before,.admin__control-table .action-delete:before,.admin__current-filters-list .action-remove:before,.admin__data-grid-action-bookmarks .action-delete:before,.admin__data-grid-action-bookmarks .action-edit:before,.admin__data-grid-action-bookmarks .action-submit:before,.admin__data-grid-action-bookmarks .admin__action-dropdown:before,.admin__data-grid-action-columns .admin__action-dropdown:before,.admin__data-grid-action-export .admin__action-dropdown:before,.admin__field-fallback-reset:before,.admin__menu .level-0>a:before,.admin__page-nav-item-message .admin__page-nav-item-message-icon,.admin__page-nav-title._collapsible:after,.data-grid-filters-action-wrap .action-default:before,.data-grid-row-changed:after,.data-grid-row-parent>td .data-grid-checkbox-cell-inner:before,.data-grid-search-control-wrap .action-submit:before,.extensions-information .list .extension-delete,.icon-failed:before,.icon-success:before,.notifications-action:before,.notifications-close:before,.page-actions .page-actions-buttons>button.action-back:before,.page-actions .page-actions-buttons>button.back:before,.page-actions>button.action-back:before,.page-actions>button.back:before,.page-title-jumbo-success:before,.search-global-label:before,.selectmenu .action-delete:before,.selectmenu .action-edit:before,.selectmenu .action-save:before,.setup-home-item:before,.sticky-header .data-grid-search-control-wrap .data-grid-search-label:before,.store-switcher .dropdown-menu .dropdown-toolbar a:before,.tooltip .help a:before,.tooltip .help span:before{-webkit-font-smoothing:antialiased;font-family:Icons;font-style:normal;font-weight:400;line-height:1;speak:none}.validation-symbol:after{color:#e22626;content:'*';font-weight:400;margin-left:3px}.abs-modal-overlay,.modals-overlay{background:rgba(0,0,0,.35);bottom:0;left:0;position:fixed;right:0;top:0}.abs-action-delete>span,.abs-visually-hidden,.action-multicheck-wrap .action-multicheck-toggle>span,.admin__actions-switch-checkbox,.admin__control-fields .admin__field:nth-child(n+2):not(.admin__field-option):not(.admin__field-group-show-label)>.admin__field-label,.admin__field-tooltip .admin__field-tooltip-action span,.customize-your-store .customize-your-store-default .legend,.extensions-information .list .extension-delete>span,.form-el-checkbox,.form-el-radio,.selectmenu .action-delete>span,.selectmenu .action-edit>span,.selectmenu .action-save>span,.selectmenu-toggle span,.tooltip .help a span,.tooltip .help span span,[class*=admin__control-grouped]>.admin__field:nth-child(n+2):not(.admin__field-option):not(.admin__field-group-show-label):not(.admin__field-date)>.admin__field-label{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.abs-visually-hidden-reset,.admin__field-group-columns>.admin__field:nth-child(n+2):not(.admin__field-option):not(.admin__field-group-show-label):not(.admin__field-date)>.admin__field-label[class]{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.abs-clearfix:after,.abs-clearfix:before,.action-multicheck-wrap:after,.action-multicheck-wrap:before,.actions-split:after,.actions-split:before,.admin__control-table-pagination:after,.admin__control-table-pagination:before,.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content:after,.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content:before,.admin__data-grid-filters-footer:after,.admin__data-grid-filters-footer:before,.admin__data-grid-filters:after,.admin__data-grid-filters:before,.admin__data-grid-header-row:after,.admin__data-grid-header-row:before,.admin__field-complex:after,.admin__field-complex:before,.modal-slide .magento-message .insert-title-inner:after,.modal-slide .magento-message .insert-title-inner:before,.modal-slide .main-col .insert-title-inner:after,.modal-slide .main-col .insert-title-inner:before,.page-actions._fixed:after,.page-actions._fixed:before,.page-content:after,.page-content:before,.page-header-actions:after,.page-header-actions:before,.page-main-actions:not(._hidden):after,.page-main-actions:not(._hidden):before{content:'';display:table}.abs-clearfix:after,.action-multicheck-wrap:after,.actions-split:after,.admin__control-table-pagination:after,.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content:after,.admin__data-grid-filters-footer:after,.admin__data-grid-filters:after,.admin__data-grid-header-row:after,.admin__field-complex:after,.modal-slide .magento-message .insert-title-inner:after,.modal-slide .main-col .insert-title-inner:after,.page-actions._fixed:after,.page-content:after,.page-header-actions:after,.page-main-actions:not(._hidden):after{clear:both}.abs-list-reset-styles{margin:0;padding:0;list-style:none}.abs-draggable-handle,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .draggable-handle,.admin__control-table .draggable-handle,.data-grid .data-grid-draggable-row-cell .draggable-handle{cursor:-webkit-grab;cursor:move;font-size:0;margin-top:-4px;padding:0 1rem 0 0;vertical-align:middle;display:inline-block;text-decoration:none}.abs-draggable-handle:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .draggable-handle:before,.admin__control-table .draggable-handle:before,.data-grid .data-grid-draggable-row-cell .draggable-handle:before{-webkit-font-smoothing:antialiased;font-size:1.8rem;line-height:inherit;color:#9e9e9e;content:'\e617';font-family:Icons;vertical-align:middle;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.abs-draggable-handle:hover:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .draggable-handle:hover:before,.admin__control-table .draggable-handle:hover:before,.data-grid .data-grid-draggable-row-cell .draggable-handle:hover:before{color:#858585}.abs-config-scope-label,.admin__field:not(.admin__field-option)>.admin__field-label span[data-config-scope]:before{bottom:-1.3rem;color:gray;content:attr(data-config-scope);font-size:1.1rem;font-weight:400;min-width:15rem;position:absolute;right:0;text-transform:lowercase}.abs-word-wrap,.admin__field:not(.admin__field-option)>.admin__field-label{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-word;-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;box-sizing:border-box}*,:after,:before{box-sizing:inherit}:focus{box-shadow:none;outline:0}._keyfocus :focus{box-shadow:0 0 0 1px #008bdb}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}embed,img,object,video{max-width:100%}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/light/opensans-300.eot);src:url(../fonts/opensans/light/opensans-300.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/light/opensans-300.woff2) format('woff2'),url(../fonts/opensans/light/opensans-300.woff) format('woff'),url(../fonts/opensans/light/opensans-300.ttf) format('truetype'),url('../fonts/opensans/light/opensans-300.svg#Open Sans') format('svg');font-weight:300;font-style:normal}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/regular/opensans-400.eot);src:url(../fonts/opensans/regular/opensans-400.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/regular/opensans-400.woff2) format('woff2'),url(../fonts/opensans/regular/opensans-400.woff) format('woff'),url(../fonts/opensans/regular/opensans-400.ttf) format('truetype'),url('../fonts/opensans/regular/opensans-400.svg#Open Sans') format('svg');font-weight:400;font-style:normal}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/semibold/opensans-600.eot);src:url(../fonts/opensans/semibold/opensans-600.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/semibold/opensans-600.woff2) format('woff2'),url(../fonts/opensans/semibold/opensans-600.woff) format('woff'),url(../fonts/opensans/semibold/opensans-600.ttf) format('truetype'),url('../fonts/opensans/semibold/opensans-600.svg#Open Sans') format('svg');font-weight:600;font-style:normal}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/bold/opensans-700.eot);src:url(../fonts/opensans/bold/opensans-700.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/bold/opensans-700.woff2) format('woff2'),url(../fonts/opensans/bold/opensans-700.woff) format('woff'),url(../fonts/opensans/bold/opensans-700.ttf) format('truetype'),url('../fonts/opensans/bold/opensans-700.svg#Open Sans') format('svg');font-weight:700;font-style:normal}html{font-size:62.5%}body{color:#333;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.36;font-size:1.4rem}h1{margin:0 0 2rem;color:#41362f;font-weight:400;line-height:1.2;font-size:2.8rem}h2{margin:0 0 2rem;color:#41362f;font-weight:400;line-height:1.2;font-size:2rem}h3{margin:0 0 2rem;color:#41362f;font-weight:600;line-height:1.2;font-size:1.7rem}h4,h5,h6{font-weight:600;margin-top:0}p{margin:0 0 1em}small{font-size:1.2rem}a{color:#008bdb;text-decoration:none}a:hover{color:#0fa7ff;text-decoration:underline}dl,ol,ul{padding-left:0}nav ol,nav ul{list-style:none;margin:0;padding:0}html{height:100%}body{background-color:#fff;min-height:100%;min-width:102.4rem}.page-wrapper{background-color:#fff;display:inline-block;margin-left:-4px;vertical-align:top;width:calc(100% - 8.8rem)}.page-content{padding-bottom:3rem;padding-left:3rem;padding-right:3rem}.notices-wrapper{margin:0 3rem}.notices-wrapper .messages{margin-bottom:0}.row{margin-left:0;margin-right:0}.row:after{clear:both;content:'';display:table}.col-l-1,.col-l-10,.col-l-11,.col-l-12,.col-l-2,.col-l-3,.col-l-4,.col-l-5,.col-l-6,.col-l-7,.col-l-8,.col-l-9,.col-m-1,.col-m-10,.col-m-11,.col-m-12,.col-m-2,.col-m-3,.col-m-4,.col-m-5,.col-m-6,.col-m-7,.col-m-8,.col-m-9,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{min-height:1px;padding-left:0;padding-right:0;position:relative}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}.row-gutter{margin-left:-1.5rem;margin-right:-1.5rem}.row-gutter>[class*=col-]{padding-left:1.5rem;padding-right:1.5rem}.abs-clearer:after,.extension-manager-content:after,.extension-manager-title:after,.form-row:after,.header:after,.nav:after,body:after{clear:both;content:'';display:table}.ng-cloak{display:none!important}.hide.hide{display:none}.show.show{display:block}.text-center{text-align:center}.text-right{text-align:right}@font-face{font-family:Icons;src:url(../fonts/icons/icons.eot);src:url(../fonts/icons/icons.eot?#iefix) format('embedded-opentype'),url(../fonts/icons/icons.woff2) format('woff2'),url(../fonts/icons/icons.woff) format('woff'),url(../fonts/icons/icons.ttf) format('truetype'),url(../fonts/icons/icons.svg#Icons) format('svg');font-weight:400;font-style:normal}[class*=icon-]{display:inline-block;line-height:1}.icon-failed:before,.icon-success:before,[class*=icon-]:after{font-family:Icons}.icon-success{color:#79a22e}.icon-success:before{content:'\e62d'}.icon-failed{color:#e22626}.icon-failed:before{content:'\e632'}.icon-success-thick:after{content:'\e62d'}.icon-collapse:after{content:'\e615'}.icon-failed-thick:after{content:'\e632'}.icon-expand:after{content:'\e616'}.icon-warning:after{content:'\e623'}.icon-failed-round,.icon-success-round{border-radius:100%;color:#fff;font-size:2.5rem;height:1em;position:relative;text-align:center;width:1em}.icon-failed-round:after,.icon-success-round:after{bottom:0;font-size:.5em;left:0;position:absolute;right:0;top:.45em}.icon-success-round{background-color:#79a22e}.icon-success-round:after{content:'\e62d'}.icon-failed-round{background-color:#e22626}.icon-failed-round:after{content:'\e632'}dl,ol,ul{margin-top:0}.list{padding-left:0}.list>li{display:block;margin-bottom:.75em;position:relative}.list>li>.icon-failed,.list>li>.icon-success{font-size:1.6em;left:-.1em;position:absolute;top:0}.list>li>.icon-success{color:#79a22e}.list>li>.icon-failed{color:#e22626}.list-item-failed,.list-item-icon,.list-item-success,.list-item-warning{padding-left:3.5rem}.list-item-failed:before,.list-item-success:before,.list-item-warning:before{left:-.1em;position:absolute}.list-item-success:before{color:#79a22e}.list-item-failed:before{color:#e22626}.list-item-warning:before{color:#ef672f}.list-definition{margin:0 0 3rem;padding:0}.list-definition>dt{clear:left;float:left}.list-definition>dd{margin-bottom:1em;margin-left:20rem}.btn-wrap{margin:0 auto}.btn-wrap .btn{width:100%}.btn{background:#e3e3e3;border:none;color:#514943;display:inline-block;font-size:1.6rem;font-weight:600;padding:.45em .9em;text-align:center}.btn:hover{background-color:#dbdbdb;color:#514943;text-decoration:none}.btn:active{background-color:#d6d6d6}.btn.disabled,.btn[disabled]{cursor:default;opacity:.5;pointer-events:none}.ie9 .btn.disabled,.ie9 .btn[disabled]{background-color:#f0f0f0;opacity:1;text-shadow:none}.btn-large{padding:.75em 1.25em}.btn-medium{font-size:1.4rem;padding:.5em 1.5em .6em}.btn-link{background-color:transparent;border:none;color:#008bdb;font-family:1.6rem;font-size:1.5rem}.btn-link:active,.btn-link:focus,.btn-link:hover{background-color:transparent;color:#0fa7ff}.btn-prime{background-color:#eb5202;color:#fff;text-shadow:1px 1px 0 rgba(0,0,0,.25)}.btn-prime:focus,.btn-prime:hover{background-color:#f65405;background-repeat:repeat-x;background-image:linear-gradient(to right,#e04f00 0,#f65405 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e04f00', endColorstr='#f65405', GradientType=1);color:#fff}.btn-prime:active{background-color:#e04f00;background-repeat:repeat-x;background-image:linear-gradient(to right,#f65405 0,#e04f00 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f65405', endColorstr='#e04f00', GradientType=1);color:#fff}.ie9 .btn-prime.disabled,.ie9 .btn-prime[disabled]{background-color:#fd6e23}.ie9 .btn-prime.disabled:active,.ie9 .btn-prime.disabled:hover,.ie9 .btn-prime[disabled]:active,.ie9 .btn-prime[disabled]:hover{background-color:#fd6e23;-webkit-filter:none;filter:none}.btn-secondary{background-color:#514943;color:#fff}.btn-secondary:hover{background-color:#5f564f;color:#fff}.btn-secondary:active,.btn-secondary:focus{background-color:#574e48;color:#fff}.ie9 .btn-secondary.disabled,.ie9 .btn-secondary[disabled]{background-color:#514943}.ie9 .btn-secondary.disabled:active,.ie9 .btn-secondary[disabled]:active{background-color:#514943;-webkit-filter:none;filter:none}[class*=btn-wrap-triangle]{overflow:hidden;position:relative}[class*=btn-wrap-triangle] .btn:after{border-style:solid;content:'';height:0;position:absolute;top:0;width:0}.btn-wrap-triangle-right{display:inline-block;padding-right:1.74rem;position:relative}.btn-wrap-triangle-right .btn{text-indent:.92rem}.btn-wrap-triangle-right .btn:after{border-color:transparent transparent transparent #e3e3e3;border-width:1.84rem 0 1.84rem 1.84rem;left:100%;margin-left:-1.74rem}.btn-wrap-triangle-right .btn:focus:after,.btn-wrap-triangle-right .btn:hover:after{border-left-color:#dbdbdb}.btn-wrap-triangle-right .btn:active:after{border-left-color:#d6d6d6}.btn-wrap-triangle-right .btn:not(.disabled):active,.btn-wrap-triangle-right .btn:not([disabled]):active{left:1px}.ie9 .btn-wrap-triangle-right .btn.disabled:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:after{border-color:transparent transparent transparent #f0f0f0}.ie9 .btn-wrap-triangle-right .btn.disabled:active:after,.ie9 .btn-wrap-triangle-right .btn.disabled:focus:after,.ie9 .btn-wrap-triangle-right .btn.disabled:hover:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:active:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:focus:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:hover:after{border-left-color:#f0f0f0}.btn-wrap-triangle-right .btn-prime:after{border-color:transparent transparent transparent #eb5202}.btn-wrap-triangle-right .btn-prime:focus:after,.btn-wrap-triangle-right .btn-prime:hover:after{border-left-color:#f65405}.btn-wrap-triangle-right .btn-prime:active:after{border-left-color:#e04f00}.btn-wrap-triangle-right .btn-prime:not(.disabled):active,.btn-wrap-triangle-right .btn-prime:not([disabled]):active{left:1px}.ie9 .btn-wrap-triangle-right .btn-prime.disabled:after,.ie9 .btn-wrap-triangle-right .btn-prime[disabled]:after{border-color:transparent transparent transparent #fd6e23}.ie9 .btn-wrap-triangle-right .btn-prime.disabled:active:after,.ie9 .btn-wrap-triangle-right .btn-prime.disabled:hover:after,.ie9 .btn-wrap-triangle-right .btn-prime[disabled]:active:after,.ie9 .btn-wrap-triangle-right .btn-prime[disabled]:hover:after{border-left-color:#fd6e23}.btn-wrap-triangle-left{display:inline-block;padding-left:1.74rem}.btn-wrap-triangle-left .btn{text-indent:-.92rem}.btn-wrap-triangle-left .btn:after{border-color:transparent #e3e3e3 transparent transparent;border-width:1.84rem 1.84rem 1.84rem 0;margin-right:-1.74rem;right:100%}.btn-wrap-triangle-left .btn:focus:after,.btn-wrap-triangle-left .btn:hover:after{border-right-color:#dbdbdb}.btn-wrap-triangle-left .btn:active:after{border-right-color:#d6d6d6}.btn-wrap-triangle-left .btn:not(.disabled):active,.btn-wrap-triangle-left .btn:not([disabled]):active{right:1px}.ie9 .btn-wrap-triangle-left .btn.disabled:after,.ie9 .btn-wrap-triangle-left .btn[disabled]:after{border-color:transparent #f0f0f0 transparent transparent}.ie9 .btn-wrap-triangle-left .btn.disabled:active:after,.ie9 .btn-wrap-triangle-left .btn.disabled:hover:after,.ie9 .btn-wrap-triangle-left .btn[disabled]:active:after,.ie9 .btn-wrap-triangle-left .btn[disabled]:hover:after{border-right-color:#f0f0f0}.btn-wrap-triangle-left .btn-prime:after{border-color:transparent #eb5202 transparent transparent}.btn-wrap-triangle-left .btn-prime:focus:after,.btn-wrap-triangle-left .btn-prime:hover:after{border-right-color:#e04f00}.btn-wrap-triangle-left .btn-prime:active:after{border-right-color:#f65405}.btn-wrap-triangle-left .btn-prime:not(.disabled):active,.btn-wrap-triangle-left .btn-prime:not([disabled]):active{right:1px}.ie9 .btn-wrap-triangle-left .btn-prime.disabled:after,.ie9 .btn-wrap-triangle-left .btn-prime[disabled]:after{border-color:transparent #fd6e23 transparent transparent}.ie9 .btn-wrap-triangle-left .btn-prime.disabled:active:after,.ie9 .btn-wrap-triangle-left .btn-prime.disabled:hover:after,.ie9 .btn-wrap-triangle-left .btn-prime[disabled]:active:after,.ie9 .btn-wrap-triangle-left .btn-prime[disabled]:hover:after{border-right-color:#fd6e23}.btn-expand{background-color:transparent;border:none;color:#303030;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:700;padding:0;position:relative}.btn-expand.expanded:after{border-color:transparent transparent #303030;border-width:0 .285em .36em}.btn-expand.expanded:hover:after{border-color:transparent transparent #3d3d3d}.btn-expand:hover{background-color:transparent;border:none;color:#3d3d3d}.btn-expand:hover:after{border-color:#3d3d3d transparent transparent}.btn-expand:after{border-color:#303030 transparent transparent;border-style:solid;border-width:.36em .285em 0;content:'';height:0;left:100%;margin-left:.5em;margin-top:-.18em;position:absolute;top:50%;width:0}[class*=col-] .form-el-input,[class*=col-] .form-el-select{width:100%}.form-fieldset{border:none;margin:0 0 1em;padding:0}.form-row{margin-bottom:2.2rem}.form-row .form-row{margin-bottom:.4rem}.form-row .form-label{display:block;font-weight:600;padding:.6rem 2.1em 0 0;text-align:right}.form-row .form-label.required{position:relative}.form-row .form-label.required:after{color:#eb5202;content:'*';font-size:1.15em;position:absolute;right:.7em;top:.5em}.form-row .form-el-checkbox+.form-label:before,.form-row .form-el-radio+.form-label:before{top:.7rem}.form-row .form-el-checkbox+.form-label:after,.form-row .form-el-radio+.form-label:after{top:1.1rem}.form-row.form-row-text{padding-top:.6rem}.form-row.form-row-text .action-sign-out{font-size:1.2rem;margin-left:1rem}.form-note{font-size:1.2rem;font-weight:600;margin-top:1rem}.form-el-dummy{display:none}.fieldset{border:0;margin:0;min-width:0;padding:0}input:not([disabled]):focus,textarea:not([disabled]):focus{box-shadow:none}.form-el-input{border:1px solid #adadad;color:#303030;padding:.35em .55em .5em}.form-el-input:hover{border-color:#949494}.form-el-input:focus{border-color:#008bdb}.form-el-input:required{box-shadow:none}.form-label{margin-bottom:.5em}[class*=form-label][for]{cursor:pointer}.form-el-insider-wrap{display:table;width:100%}.form-el-insider-input{display:table-cell;width:100%}.form-el-insider{border-radius:2px;display:table-cell;padding:.43em .55em .5em 0;vertical-align:top}.form-legend,.form-legend-expand,.form-legend-light{display:block;margin:0}.form-legend,.form-legend-expand{font-size:1.25em;font-weight:600;margin-bottom:2.5em;padding-top:1.5em}.form-legend{border-top:1px solid #ccc;width:100%}.form-legend-light{font-size:1em;margin-bottom:1.5em}.form-legend-expand{cursor:pointer;transition:opacity .2s linear}.form-legend-expand:hover{opacity:.85}.form-legend-expand.expanded:after{content:'\e615'}.form-legend-expand:after{content:'\e616';font-family:Icons;font-size:1.15em;font-weight:400;margin-left:.5em;vertical-align:sub}.form-el-checkbox.disabled+.form-label,.form-el-checkbox.disabled+.form-label:before,.form-el-checkbox[disabled]+.form-label,.form-el-checkbox[disabled]+.form-label:before,.form-el-radio.disabled+.form-label,.form-el-radio.disabled+.form-label:before,.form-el-radio[disabled]+.form-label,.form-el-radio[disabled]+.form-label:before{cursor:default;opacity:.5;pointer-events:none}.form-el-checkbox:not(.disabled)+.form-label:hover:before,.form-el-checkbox:not([disabled])+.form-label:hover:before,.form-el-radio:not(.disabled)+.form-label:hover:before,.form-el-radio:not([disabled])+.form-label:hover:before{border-color:#514943}.form-el-checkbox+.form-label,.form-el-radio+.form-label{font-weight:400;padding-left:2em;padding-right:0;position:relative;text-align:left;transition:border-color .1s linear}.form-el-checkbox+.form-label:before,.form-el-radio+.form-label:before{border:1px solid;content:'';left:0;position:absolute;top:.1rem;transition:border-color .1s linear}.form-el-checkbox+.form-label:before{background-color:#fff;border-color:#adadad;border-radius:2px;font-size:1.2rem;height:1.6rem;line-height:1.2;width:1.6rem}.form-el-checkbox:checked+.form-label::before{content:'\e62d';font-family:Icons}.form-el-radio+.form-label:before{background-color:#fff;border:1px solid #adadad;border-radius:100%;height:1.8rem;width:1.8rem}.form-el-radio+.form-label:after{background:0 0;border:.5rem solid transparent;border-radius:100%;content:'';height:0;left:.4rem;position:absolute;top:.5rem;transition:background .3s linear;width:0}.form-el-radio:checked+.form-label{cursor:default}.form-el-radio:checked+.form-label:after{border-color:#514943}.form-select-label{border:1px solid #adadad;color:#303030;cursor:pointer;display:block;overflow:hidden;position:relative;z-index:0}.form-select-label:hover,.form-select-label:hover:after{border-color:#949494}.form-select-label:active,.form-select-label:active:after,.form-select-label:focus,.form-select-label:focus:after{border-color:#008bdb}.form-select-label:after{background:#e3e3e3;border-left:1px solid #adadad;bottom:0;content:'';position:absolute;right:0;top:0;width:2.36em;z-index:-2}.ie9 .form-select-label:after{display:none}.form-select-label:before{border-color:#303030 transparent transparent;border-style:solid;border-width:5px 4px 0;content:'';height:0;margin-right:-4px;margin-top:-2.5px;position:absolute;right:1.18em;top:50%;width:0;z-index:-1}.ie9 .form-select-label:before{display:none}.form-select-label .form-el-select{background:0 0;border:none;border-radius:0;content:'';display:block;margin:0;padding:.35em calc(2.36em + 10%) .5em .55em;width:110%}.ie9 .form-select-label .form-el-select{padding-right:.55em;width:100%}.form-select-label .form-el-select::-ms-expand{display:none}.form-el-select{background:#fff;border:1px solid #adadad;border-radius:2px;color:#303030;display:block;padding:.35em .55em}.multiselect-custom{border:1px solid #adadad;height:45.2rem;margin:0 0 1.5rem;overflow:auto;position:relative}.multiselect-custom ul{margin:0;padding:0;list-style:none;min-width:29rem}.multiselect-custom .item{padding:1rem 1.4rem}.multiselect-custom .selected{background-color:#e0f6fe}.multiselect-custom .form-label{margin-bottom:0}[class*=form-el-].invalid{border-color:#e22626}[class*=form-el-].invalid+.error-container{display:block}.error-container{background-color:#fffbbb;border:1px solid #ee7d7d;color:#514943;display:none;font-size:1.19rem;margin-top:.2rem;padding:.8rem 1rem .9rem}.check-result-message{margin-left:.5em;min-height:3.68rem;-ms-align-items:center;-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex}.check-result-text{margin-left:.5em}body:not([class]){min-width:0}.container{display:block;margin:0 auto 4rem;max-width:100rem;padding:0}.abs-action-delete,.action-close:before,.action-next:before,.action-previous:before,.admin-user .admin__action-dropdown:before,.admin__action-multiselect-dropdown:before,.admin__action-multiselect-search-label:before,.admin__control-checkbox+label:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:before,.admin__control-table .action-delete:before,.admin__current-filters-list .action-remove:before,.admin__data-grid-action-bookmarks .action-delete:before,.admin__data-grid-action-bookmarks .action-edit:before,.admin__data-grid-action-bookmarks .action-submit:before,.admin__data-grid-action-bookmarks .admin__action-dropdown:before,.admin__data-grid-action-columns .admin__action-dropdown:before,.admin__data-grid-action-export .admin__action-dropdown:before,.admin__field-fallback-reset:before,.admin__menu .level-0>a:before,.admin__page-nav-item-message .admin__page-nav-item-message-icon,.admin__page-nav-title._collapsible:after,.data-grid-filters-action-wrap .action-default:before,.data-grid-row-changed:after,.data-grid-row-parent>td .data-grid-checkbox-cell-inner:before,.data-grid-search-control-wrap .action-submit:before,.extensions-information .list .extension-delete,.icon-failed:before,.icon-success:before,.notifications-action:before,.notifications-close:before,.page-actions .page-actions-buttons>button.action-back:before,.page-actions .page-actions-buttons>button.back:before,.page-actions>button.action-back:before,.page-actions>button.back:before,.page-title-jumbo-success:before,.search-global-label:before,.selectmenu .action-delete:before,.selectmenu .action-edit:before,.selectmenu .action-save:before,.setup-home-item:before,.sticky-header .data-grid-search-control-wrap .data-grid-search-label:before,.store-switcher .dropdown-menu .dropdown-toolbar a:before,.tooltip .help a:before,.tooltip .help span:before{-webkit-font-smoothing:antialiased;font-family:Icons;font-style:normal;font-weight:400;line-height:1;speak:none}.text-stretch{margin-bottom:1.5em}.page-title-jumbo{font-size:4rem;font-weight:300;letter-spacing:-.05em;margin-bottom:2.9rem}.page-title-jumbo-success:before{color:#79a22e;content:'\e62d';font-size:3.9rem;margin-left:-.3rem;margin-right:2.4rem}.list{margin-bottom:3rem}.list-dot .list-item{display:list-item;list-style-position:inside;margin-bottom:1.2rem}.list-title{color:#333;font-size:1.4rem;font-weight:700;letter-spacing:.025em;margin-bottom:1.2rem}.list-item-failed:before,.list-item-success:before,.list-item-warning:before{font-family:Icons;font-size:1.6rem;top:0}.list-item-success:before{content:'\e62d';font-size:1.6rem}.list-item-failed:before{content:'\e632';font-size:1.4rem;left:.1rem;top:.2rem}.list-item-warning:before{content:'\e623';font-size:1.3rem;left:.2rem}.form-wrap{margin-bottom:3.6rem;padding-top:2.1rem}.form-el-label-horizontal{display:inline-block;font-size:1.3rem;font-weight:600;letter-spacing:.025em;margin-bottom:.4rem;margin-left:.4rem}.app-updater{min-width:768px}body._has-modal{height:100%;overflow:hidden;width:100%}.modals-overlay{z-index:899}.modal-popup,.modal-slide{bottom:0;min-width:0;position:fixed;right:0;top:0;visibility:hidden}.modal-popup._show,.modal-slide._show{visibility:visible}.modal-popup._show .modal-inner-wrap,.modal-slide._show .modal-inner-wrap{-ms-transform:translate(0,0);transform:translate(0,0)}.modal-popup .modal-inner-wrap,.modal-slide .modal-inner-wrap{background-color:#fff;box-shadow:0 0 12px 2px rgba(0,0,0,.35);opacity:1;pointer-events:auto}.modal-slide{left:14.8rem;z-index:900}.modal-slide._show .modal-inner-wrap{-ms-transform:translateX(0);transform:translateX(0)}.modal-slide .modal-inner-wrap{height:100%;overflow-y:auto;position:static;-ms-transform:translateX(100%);transform:translateX(100%);transition-duration:.3s;transition-property:transform,visibility;transition-timing-function:ease-in-out;width:auto}.modal-slide._inner-scroll .modal-inner-wrap{overflow-y:visible;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.modal-slide._inner-scroll .modal-footer,.modal-slide._inner-scroll .modal-header{-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0}.modal-slide._inner-scroll .modal-content{overflow-y:auto}.modal-slide._inner-scroll .modal-footer{margin-top:auto}.modal-slide .modal-content,.modal-slide .modal-footer,.modal-slide .modal-header{padding:0 2.6rem 2.6rem}.modal-slide .modal-header{padding-bottom:2.1rem;padding-top:2.1rem}.modal-popup{z-index:900;left:0;overflow-y:auto}.modal-popup._show .modal-inner-wrap{-ms-transform:translateY(0);transform:translateY(0)}.modal-popup .modal-inner-wrap{margin:5rem auto;width:75%;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;box-sizing:border-box;height:auto;left:0;position:absolute;right:0;-ms-transform:translateY(-200%);transform:translateY(-200%);transition-duration:.2s;transition-property:transform,visibility;transition-timing-function:ease}.modal-popup._inner-scroll{overflow-y:visible}.ie10 .modal-popup._inner-scroll,.ie9 .modal-popup._inner-scroll{overflow-y:auto}.modal-popup._inner-scroll .modal-inner-wrap{max-height:90%}.ie10 .modal-popup._inner-scroll .modal-inner-wrap,.ie9 .modal-popup._inner-scroll .modal-inner-wrap{max-height:none}.modal-popup._inner-scroll .modal-content{overflow-y:auto}.modal-popup .modal-content,.modal-popup .modal-footer,.modal-popup .modal-header{padding-left:3rem;padding-right:3rem}.modal-popup .modal-footer,.modal-popup .modal-header{-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0}.modal-popup .modal-header{padding-bottom:1.2rem;padding-top:3rem}.modal-popup .modal-footer{margin-top:auto;padding-bottom:3rem}.modal-popup .modal-footer-actions{text-align:right}.admin__action-dropdown-wrap{display:inline-block;position:relative}.admin__action-dropdown-wrap .admin__action-dropdown-text:after{left:-6px;right:0}.admin__action-dropdown-wrap .admin__action-dropdown-menu{left:auto;right:0}.admin__action-dropdown-wrap._active .admin__action-dropdown,.admin__action-dropdown-wrap.active .admin__action-dropdown{border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.admin__action-dropdown-wrap._active .admin__action-dropdown-text:after,.admin__action-dropdown-wrap.active .admin__action-dropdown-text:after{background-color:#fff;content:'';height:6px;position:absolute;top:100%}.admin__action-dropdown-wrap._active .admin__action-dropdown-menu,.admin__action-dropdown-wrap.active .admin__action-dropdown-menu{display:block}.admin__action-dropdown-wrap._disabled .admin__action-dropdown{cursor:default}.admin__action-dropdown-wrap._disabled:hover .admin__action-dropdown{color:#333}.admin__action-dropdown{background-color:#fff;border:1px solid transparent;border-bottom:none;border-radius:0;box-shadow:none;color:#333;display:inline-block;font-size:1.3rem;font-weight:400;letter-spacing:-.025em;padding:.7rem 3.3rem .8rem 1.5rem;position:relative;vertical-align:baseline;z-index:2}.admin__action-dropdown._active:after,.admin__action-dropdown.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin__action-dropdown:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;top:50%;transition:all .2s linear;width:0}._active .admin__action-dropdown:after,.active .admin__action-dropdown:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin__action-dropdown:hover:after{border-color:#000 transparent transparent}.admin__action-dropdown:focus,.admin__action-dropdown:hover{background-color:#fff;color:#000;text-decoration:none}.admin__action-dropdown:after{right:1.5rem}.admin__action-dropdown:before{margin-right:1rem}.admin__action-dropdown-menu{background-color:#fff;border:1px solid #007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5);display:none;line-height:1.36;margin-top:-1px;min-width:120%;padding:.5rem 1rem;position:absolute;top:100%;transition:all .15s ease;z-index:1}.admin__action-dropdown-menu>li{display:block}.admin__action-dropdown-menu>li>a{color:#333;display:block;text-decoration:none;padding:.6rem .5rem}.selectmenu{display:inline-block;position:relative;text-align:left;z-index:1}.selectmenu._active{border-color:#007bdb;z-index:500}.selectmenu .action-delete,.selectmenu .action-edit,.selectmenu .action-save{background-color:transparent;border-color:transparent;box-shadow:none;padding:0 1rem}.selectmenu .action-delete:hover,.selectmenu .action-edit:hover,.selectmenu .action-save:hover{background-color:transparent;border-color:transparent;box-shadow:none}.selectmenu .action-delete:before,.selectmenu .action-edit:before,.selectmenu .action-save:before{content:'\e630'}.selectmenu .action-delete,.selectmenu .action-edit{border:0 solid #fff;border-left-width:1px;bottom:0;position:absolute;right:0;top:0;z-index:1}.selectmenu .action-delete:hover,.selectmenu .action-edit:hover{border:0 solid #fff;border-left-width:1px}.selectmenu .action-save:before{content:'\e625'}.selectmenu .action-edit:before{content:'\e631'}.selectmenu-value{display:inline-block}.selectmenu-value input[type=text]{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;border:0;display:inline;margin:0;width:6rem}body._keyfocus .selectmenu-value input[type=text]:focus{box-shadow:none}.selectmenu-toggle{padding-right:3rem;background:0 0;border-width:0;bottom:0;float:right;position:absolute;right:0;top:0;width:0}.selectmenu-toggle._active:after,.selectmenu-toggle.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.selectmenu-toggle:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.1rem;top:50%;transition:all .2s linear;width:0}._active .selectmenu-toggle:after,.active .selectmenu-toggle:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.selectmenu-toggle:hover:after{border-color:#000 transparent transparent}.selectmenu-toggle:active,.selectmenu-toggle:focus,.selectmenu-toggle:hover{background:0 0}.selectmenu._active .selectmenu-toggle:before{border-color:#007bdb}body._keyfocus .selectmenu-toggle:focus{box-shadow:none}.selectmenu-toggle:before{background:#e3e3e3;border-left:1px solid #adadad;bottom:0;content:'';display:block;position:absolute;right:0;top:0;width:3.2rem}.selectmenu-items{background:#fff;border:1px solid #007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5);display:none;float:left;left:-1px;margin-top:3px;max-width:20rem;min-width:calc(100% + 2px);position:absolute;top:100%}.selectmenu-items._active{display:block}.selectmenu-items ul{float:left;list-style-type:none;margin:0;min-width:100%;padding:0}.selectmenu-items li{-webkit-flex-direction:row;display:flex;-ms-flex-direction:row;flex-direction:row;transition:background .2s linear}.selectmenu-items li:hover{background:#e3e3e3}.selectmenu-items li:last-child .selectmenu-item-action,.selectmenu-items li:last-child .selectmenu-item-action:visited{color:#008bdb;text-decoration:none}.selectmenu-items li:last-child .selectmenu-item-action:hover{color:#0fa7ff;text-decoration:underline}.selectmenu-items li:last-child .selectmenu-item-action:active{color:#ff5501;text-decoration:underline}.selectmenu-item{position:relative;width:100%;z-index:1}li._edit>.selectmenu-item{display:none}.selectmenu-item-edit{display:none;padding:.3rem 4rem .3rem .4rem;position:relative;white-space:nowrap;z-index:1}li:last-child .selectmenu-item-edit{padding-right:.4rem}.selectmenu-item-edit .admin__control-text{margin:0;width:5.4rem}li._edit .selectmenu-item-edit{display:block}.selectmenu-item-action{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;background:0 0;border:0;color:#333;display:block;font-size:1.4rem;font-weight:400;min-width:100%;padding:1rem 6rem 1rem 1.5rem;text-align:left;transition:background .2s linear;width:5rem}.selectmenu-item-action:focus,.selectmenu-item-action:hover{background:#e3e3e3}.abs-actions-split-xl .action-default,.page-actions .actions-split .action-default{margin-right:4rem}.abs-actions-split-xl .action-toggle,.page-actions .actions-split .action-toggle{padding-right:4rem}.abs-actions-split-xl .action-toggle:after,.page-actions .actions-split .action-toggle:after{border-width:.9rem .6rem 0;margin-top:-.3rem;right:1.4rem}.actions-split{position:relative;z-index:400}.actions-split._active,.actions-split.active,.actions-split:hover{box-shadow:0 0 0 1px #007bdb}.actions-split._active .action-toggle.action-primary,.actions-split._active .action-toggle.primary,.actions-split.active .action-toggle.action-primary,.actions-split.active .action-toggle.primary{background-color:#ba4000;border-color:#ba4000}.actions-split._active .dropdown-menu,.actions-split.active .dropdown-menu{opacity:1;visibility:visible;display:block}.actions-split .action-default,.actions-split .action-toggle{float:left;margin:0}.actions-split .action-default._active,.actions-split .action-default.active,.actions-split .action-default:hover,.actions-split .action-toggle._active,.actions-split .action-toggle.active,.actions-split .action-toggle:hover{box-shadow:none}.actions-split .action-default{margin-right:3.2rem;min-width:9.3rem}.actions-split .action-toggle{padding-right:3.2rem;border-left-color:rgba(0,0,0,.2);bottom:0;padding-left:0;position:absolute;right:0;top:0}.actions-split .action-toggle._active:after,.actions-split .action-toggle.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.actions-split .action-toggle:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.2rem;top:50%;transition:all .2s linear;width:0}._active .actions-split .action-toggle:after,.active .actions-split .action-toggle:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.actions-split .action-toggle:hover:after{border-color:#000 transparent transparent}.actions-split .action-toggle.action-primary:after,.actions-split .action-toggle.action-secondary:after,.actions-split .action-toggle.primary:after,.actions-split .action-toggle.secondary:after{border-color:#fff transparent transparent}.actions-split .action-toggle>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.action-select-wrap{display:inline-block;position:relative}.action-select-wrap .action-select{padding-right:3.2rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background-color:#fff;font-weight:400;text-align:left}.action-select-wrap .action-select._active:after,.action-select-wrap .action-select.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-select-wrap .action-select:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.2rem;top:50%;transition:all .2s linear;width:0}._active .action-select-wrap .action-select:after,.active .action-select-wrap .action-select:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-select-wrap .action-select:hover:after{border-color:#000 transparent transparent}.action-select-wrap .action-select:hover,.action-select-wrap .action-select:hover:before{border-color:#878787}.action-select-wrap .action-select:before{background-color:#e3e3e3;border:1px solid #adadad;bottom:0;content:'';position:absolute;right:0;top:0;width:3.2rem}.action-select-wrap .action-select._active{border-color:#007bdb}.action-select-wrap .action-select._active:before{border-color:#007bdb #007bdb #007bdb #adadad}.action-select-wrap .action-select[disabled]{color:#333}.action-select-wrap .action-select[disabled]:after{border-color:#333 transparent transparent}.action-select-wrap._active{z-index:500}.action-select-wrap._active .action-select,.action-select-wrap._active .action-select:before{border-color:#007bdb}.action-select-wrap._active .action-select:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-select-wrap .abs-action-menu .action-submenu,.action-select-wrap .abs-action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu,.action-select-wrap .action-menu .action-submenu,.action-select-wrap .actions-split .action-menu .action-submenu,.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu,.action-select-wrap .actions-split .dropdown-menu .action-submenu,.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{max-height:45rem;overflow-y:auto}.action-select-wrap .abs-action-menu .action-submenu ._disabled:hover,.action-select-wrap .abs-action-menu .action-submenu .action-submenu ._disabled:hover,.action-select-wrap .action-menu ._disabled:hover,.action-select-wrap .action-menu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .action-menu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .dropdown-menu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu ._disabled:hover{background:#fff}.action-select-wrap .abs-action-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .abs-action-menu .action-submenu .action-submenu ._disabled .action-menu-item,.action-select-wrap .action-menu ._disabled .action-menu-item,.action-select-wrap .action-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .action-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .dropdown-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu ._disabled .action-menu-item{cursor:default;opacity:.5}.action-select-wrap .action-menu-items{left:0;position:absolute;right:0;top:100%}.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu,.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.action-menu,.action-select-wrap .action-menu-items>.action-menu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu .action-submenu{min-width:100%;position:static}.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.action-menu .action-submenu,.action-select-wrap .action-menu-items>.action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu .action-submenu .action-submenu{position:absolute}.action-multicheck-wrap{display:inline-block;height:1.6rem;padding-top:1px;position:relative;width:3.1rem;z-index:200}.action-multicheck-wrap:hover .action-multicheck-toggle,.action-multicheck-wrap:hover .admin__control-checkbox+label:before{border-color:#878787}.action-multicheck-wrap._active .action-multicheck-toggle,.action-multicheck-wrap._active .admin__control-checkbox+label:before{border-color:#007bdb}.action-multicheck-wrap._active .abs-action-menu .action-submenu,.action-multicheck-wrap._active .abs-action-menu .action-submenu .action-submenu,.action-multicheck-wrap._active .action-menu,.action-multicheck-wrap._active .action-menu .action-submenu,.action-multicheck-wrap._active .actions-split .action-menu .action-submenu,.action-multicheck-wrap._active .actions-split .action-menu .action-submenu .action-submenu,.action-multicheck-wrap._active .actions-split .dropdown-menu .action-submenu,.action-multicheck-wrap._active .actions-split .dropdown-menu .action-submenu .action-submenu{opacity:1;visibility:visible;display:block}.action-multicheck-wrap._disabled .admin__control-checkbox+label:before{background-color:#fff}.action-multicheck-wrap._disabled .action-multicheck-toggle,.action-multicheck-wrap._disabled .admin__control-checkbox+label:before{border-color:#adadad;opacity:1}.action-multicheck-wrap .action-multicheck-toggle,.action-multicheck-wrap .admin__control-checkbox,.action-multicheck-wrap .admin__control-checkbox+label{float:left}.action-multicheck-wrap .action-multicheck-toggle{border-radius:0 1px 1px 0;height:1.6rem;margin-left:-1px;padding:0;position:relative;transition:border-color .1s linear;width:1.6rem}.action-multicheck-wrap .action-multicheck-toggle._active:after,.action-multicheck-wrap .action-multicheck-toggle.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-multicheck-wrap .action-multicheck-toggle:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;top:50%;transition:all .2s linear;width:0}._active .action-multicheck-wrap .action-multicheck-toggle:after,.active .action-multicheck-wrap .action-multicheck-toggle:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-multicheck-wrap .action-multicheck-toggle:hover:after{border-color:#000 transparent transparent}.action-multicheck-wrap .action-multicheck-toggle:focus{border-color:#007bdb}.action-multicheck-wrap .action-multicheck-toggle:after{right:.3rem}.action-multicheck-wrap .abs-action-menu .action-submenu,.action-multicheck-wrap .abs-action-menu .action-submenu .action-submenu,.action-multicheck-wrap .action-menu,.action-multicheck-wrap .action-menu .action-submenu,.action-multicheck-wrap .actions-split .action-menu .action-submenu,.action-multicheck-wrap .actions-split .action-menu .action-submenu .action-submenu,.action-multicheck-wrap .actions-split .dropdown-menu .action-submenu,.action-multicheck-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{left:-1.1rem;margin-top:1px;right:auto;text-align:left}.action-multicheck-wrap .action-menu-item{white-space:nowrap}.admin__action-multiselect-wrap{display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.admin__action-multiselect-wrap.action-select-wrap:focus{box-shadow:none}.admin__action-multiselect-wrap.action-select-wrap .abs-action-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .abs-action-menu .action-submenu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .action-menu,.admin__action-multiselect-wrap.action-select-wrap .action-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .action-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .dropdown-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{max-height:none;overflow-y:inherit}.admin__action-multiselect-wrap .action-menu-item{transition:background-color .1s linear}.admin__action-multiselect-wrap .action-menu-item._selected{background-color:#e0f6fe}.admin__action-multiselect-wrap .action-menu-item._hover{background-color:#e3e3e3}.admin__action-multiselect-wrap .action-menu-item._unclickable{cursor:default}.admin__action-multiselect-wrap .admin__action-multiselect{border:1px solid #adadad;cursor:pointer;display:block;min-height:3.2rem;padding-right:3.6rem;white-space:normal}.admin__action-multiselect-wrap .admin__action-multiselect:after{bottom:1.25rem;top:auto}.admin__action-multiselect-wrap .admin__action-multiselect:before{height:3.3rem;top:auto}.admin__control-table-wrapper .admin__action-multiselect-wrap{position:static}.admin__control-table-wrapper .admin__action-multiselect-wrap .admin__action-multiselect{position:relative}.admin__control-table-wrapper .admin__action-multiselect-wrap .admin__action-multiselect:before{right:-1px;top:-1px}.admin__control-table-wrapper .admin__action-multiselect-wrap .abs-action-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .abs-action-menu .action-submenu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .action-menu,.admin__control-table-wrapper .admin__action-multiselect-wrap .action-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .action-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .action-menu .action-submenu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .dropdown-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{left:auto;min-width:34rem;right:auto;top:auto;z-index:1}.admin__action-multiselect-wrap .admin__action-multiselect-item-path{color:#a79d95;font-size:1.2rem;font-weight:400;padding-left:1rem}.admin__action-multiselect-actions-wrap{border-top:1px solid #e3e3e3;margin:0 1rem;padding:1rem 0;text-align:center}.admin__action-multiselect-actions-wrap .action-default{font-size:1.3rem;min-width:13rem}.admin__action-multiselect-text{padding:.6rem 1rem}.abs-action-menu .action-submenu,.abs-action-menu .action-submenu .action-submenu,.action-menu,.action-menu .action-submenu,.actions-split .action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu,.actions-split .dropdown-menu .action-submenu .action-submenu{text-align:left}.admin__action-multiselect-label{cursor:pointer;position:relative;z-index:1}.admin__action-multiselect-label:before{margin-right:.5rem}._unclickable .admin__action-multiselect-label{cursor:default;font-weight:700}.admin__action-multiselect-search-wrap{border-bottom:1px solid #e3e3e3;margin:0 1rem;padding:1rem 0;position:relative}.admin__action-multiselect-search{padding-right:3rem;width:100%}.admin__action-multiselect-search-label{display:block;font-size:1.5rem;height:1em;overflow:hidden;position:absolute;right:2.2rem;top:1.7rem;width:1em}.admin__action-multiselect-search-label:before{content:'\e60c'}.admin__action-multiselect-search-count{color:#a79d95;margin-top:1rem}.admin__action-multiselect-menu-inner{margin-bottom:0;max-height:46rem;overflow-y:auto}.admin__action-multiselect-menu-inner .admin__action-multiselect-menu-inner{list-style:none;max-height:none;overflow:hidden;padding-left:2.2rem}.admin__action-multiselect-menu-inner ._hidden{display:none}.admin__action-multiselect-crumb{background-color:#f5f5f5;border:1px solid #a79d95;border-radius:1px;display:inline-block;font-size:1.2rem;margin:.3rem -4px .3rem .3rem;padding:.3rem 2.4rem .4rem 1rem;position:relative;transition:border-color .1s linear}.admin__action-multiselect-crumb:hover{border-color:#908379}.admin__action-multiselect-crumb .action-close{bottom:0;font-size:.5em;position:absolute;right:0;top:0;width:2rem}.admin__action-multiselect-crumb .action-close:hover{color:#000}.admin__action-multiselect-crumb .action-close:active,.admin__action-multiselect-crumb .action-close:focus{background-color:transparent}.admin__action-multiselect-crumb .action-close:active{-ms-transform:scale(0.9);transform:scale(0.9)}.admin__action-multiselect-tree .abs-action-menu .action-submenu,.admin__action-multiselect-tree .abs-action-menu .action-submenu .action-submenu,.admin__action-multiselect-tree .action-menu,.admin__action-multiselect-tree .action-menu .action-submenu,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu .action-submenu,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu .action-submenu{min-width:34.7rem}.admin__action-multiselect-tree .abs-action-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .abs-action-menu .action-submenu .action-submenu .action-menu-item,.admin__action-multiselect-tree .action-menu .action-menu-item,.admin__action-multiselect-tree .action-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu .action-submenu .action-menu-item{margin-top:.1rem}.admin__action-multiselect-tree .action-menu-item{margin-left:4.2rem;position:relative}.admin__action-multiselect-tree .action-menu-item._expended:before{border-left:1px dashed #a79d95;bottom:0;content:'';left:-1rem;position:absolute;top:1rem;width:1px}.admin__action-multiselect-tree .action-menu-item._expended .admin__action-multiselect-dropdown:before{content:'\e615'}.admin__action-multiselect-tree .action-menu-item._with-checkbox .admin__action-multiselect-label{padding-left:2.6rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner{position:relative}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner .admin__action-multiselect-menu-inner{padding-left:3.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner .admin__action-multiselect-menu-inner:before{left:4.3rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item{position:relative}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:last-child:before{height:2.1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:after,.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:before{content:'';left:0;position:absolute}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:after{border-top:1px dashed #a79d95;height:1px;top:2.1rem;width:5.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:before{border-left:1px dashed #a79d95;height:100%;top:0;width:1px}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._parent:after{width:4.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root{margin-left:-1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:after{left:3.2rem;width:2.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:before{left:3.2rem;top:1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root._parent:after{display:none}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:first-child:before{top:2.1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:last-child:before{height:1rem}.admin__action-multiselect-tree .admin__action-multiselect-label{line-height:2.2rem;vertical-align:middle;word-break:break-all}.admin__action-multiselect-tree .admin__action-multiselect-label:before{left:0;position:absolute;top:.4rem}.admin__action-multiselect-dropdown{border-radius:50%;height:2.2rem;left:-2.2rem;position:absolute;top:1rem;width:2.2rem;z-index:1}.admin__action-multiselect-dropdown:before{background:#fff;color:#a79d95;content:'\e616';font-size:2.2rem}.admin__actions-switch{display:inline-block;position:relative;vertical-align:middle}.admin__field-control .admin__actions-switch{line-height:3.2rem}.admin__actions-switch+.admin__field-service{min-width:34rem}._disabled .admin__actions-switch-checkbox+.admin__actions-switch-label,.admin__actions-switch-checkbox.disabled+.admin__actions-switch-label{cursor:not-allowed;opacity:.5;pointer-events:none}.admin__actions-switch-checkbox:checked+.admin__actions-switch-label:before{left:15px}.admin__actions-switch-checkbox:checked+.admin__actions-switch-label:after{background:#79a22e}.admin__actions-switch-checkbox:checked+.admin__actions-switch-label .admin__actions-switch-text:before{content:attr(data-text-on)}.admin__actions-switch-checkbox:focus+.admin__actions-switch-label:after,.admin__actions-switch-checkbox:focus+.admin__actions-switch-label:before{border-color:#007bdb}._error .admin__actions-switch-checkbox+.admin__actions-switch-label:after,._error .admin__actions-switch-checkbox+.admin__actions-switch-label:before{border-color:#e22626}.admin__actions-switch-label{cursor:pointer;display:inline-block;height:22px;line-height:22px;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle}.admin__actions-switch-label:after,.admin__actions-switch-label:before{left:0;position:absolute;right:auto;top:0}.admin__actions-switch-label:before{background:#fff;border:1px solid #aaa6a0;border-radius:100%;content:'';display:block;height:22px;transition:left .2s ease-in 0s;width:22px;z-index:1}.admin__actions-switch-label:after{background:#e3e3e3;border:1px solid #aaa6a0;border-radius:12px;content:'';display:block;height:22px;transition:background .2s ease-in 0s;vertical-align:middle;width:37px;z-index:0}.admin__actions-switch-text:before{content:attr(data-text-off);padding-left:47px;white-space:nowrap}.abs-action-delete,.abs-action-reset,.action-close,.admin__field-fallback-reset,.extensions-information .list .extension-delete,.notifications-close,.search-global-field._active .search-global-action{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;padding:0}.abs-action-delete:hover,.abs-action-reset:hover,.action-close:hover,.admin__field-fallback-reset:hover,.extensions-information .list .extension-delete:hover,.notifications-close:hover,.search-global-field._active .search-global-action:hover{background-color:transparent;border:none;box-shadow:none}.abs-action-default,.abs-action-pattern,.abs-action-primary,.abs-action-quaternary,.abs-action-secondary,.abs-action-tertiary,.action-default,.action-primary,.action-quaternary,.action-secondary,.action-tertiary,.modal-popup .modal-footer .action-primary,.modal-popup .modal-footer .action-secondary,.page-actions .page-actions-buttons>button,.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.action-secondary,.page-actions .page-actions-buttons>button.primary,.page-actions>button,.page-actions>button.action-primary,.page-actions>button.action-secondary,.page-actions>button.primary,button,button.primary,button.secondary,button.tertiary{border:1px solid;border-radius:0;display:inline-block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:600;line-height:1.36;padding:.6rem 1em;text-align:center;vertical-align:baseline}.abs-action-default.disabled,.abs-action-default[disabled],.abs-action-pattern.disabled,.abs-action-pattern[disabled],.abs-action-primary.disabled,.abs-action-primary[disabled],.abs-action-quaternary.disabled,.abs-action-quaternary[disabled],.abs-action-secondary.disabled,.abs-action-secondary[disabled],.abs-action-tertiary.disabled,.abs-action-tertiary[disabled],.action-default.disabled,.action-default[disabled],.action-primary.disabled,.action-primary[disabled],.action-quaternary.disabled,.action-quaternary[disabled],.action-secondary.disabled,.action-secondary[disabled],.action-tertiary.disabled,.action-tertiary[disabled],.modal-popup .modal-footer .action-primary.disabled,.modal-popup .modal-footer .action-primary[disabled],.modal-popup .modal-footer .action-secondary.disabled,.modal-popup .modal-footer .action-secondary[disabled],.page-actions .page-actions-buttons>button.action-primary.disabled,.page-actions .page-actions-buttons>button.action-primary[disabled],.page-actions .page-actions-buttons>button.action-secondary.disabled,.page-actions .page-actions-buttons>button.action-secondary[disabled],.page-actions .page-actions-buttons>button.disabled,.page-actions .page-actions-buttons>button.primary.disabled,.page-actions .page-actions-buttons>button.primary[disabled],.page-actions .page-actions-buttons>button[disabled],.page-actions>button.action-primary.disabled,.page-actions>button.action-primary[disabled],.page-actions>button.action-secondary.disabled,.page-actions>button.action-secondary[disabled],.page-actions>button.disabled,.page-actions>button.primary.disabled,.page-actions>button.primary[disabled],.page-actions>button[disabled],button.disabled,button.primary.disabled,button.primary[disabled],button.secondary.disabled,button.secondary[disabled],button.tertiary.disabled,button.tertiary[disabled],button[disabled]{cursor:default;opacity:.5;pointer-events:none}.abs-action-l,.modal-popup .modal-footer .action-primary,.modal-popup .modal-footer .action-secondary,.page-actions .page-actions-buttons>button,.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.action-secondary,.page-actions .page-actions-buttons>button.primary,.page-actions button,.page-actions>button.action-primary,.page-actions>button.action-secondary,.page-actions>button.primary{font-size:1.6rem;letter-spacing:.025em;padding-bottom:.6875em;padding-top:.6875em}.abs-action-delete,.extensions-information .list .extension-delete{display:inline-block;font-size:1.6rem;margin-left:1.2rem;padding-top:.7rem;text-decoration:none;vertical-align:middle}.abs-action-delete:after,.extensions-information .list .extension-delete:after{color:#666;content:'\e630'}.abs-action-delete:hover:after,.extensions-information .list .extension-delete:hover:after{color:#35302c}.abs-action-button-as-link,.action-advanced,.data-grid .action-delete{line-height:1.36;padding:0;color:#008bdb;text-decoration:none;background:0 0;border:0;display:inline;font-weight:400;border-radius:0}.abs-action-button-as-link:visited,.action-advanced:visited,.data-grid .action-delete:visited{color:#008bdb;text-decoration:none}.abs-action-button-as-link:hover,.action-advanced:hover,.data-grid .action-delete:hover{text-decoration:underline}.abs-action-button-as-link:active,.action-advanced:active,.data-grid .action-delete:active{color:#ff5501;text-decoration:underline}.abs-action-button-as-link:hover,.action-advanced:hover,.data-grid .action-delete:hover{color:#0fa7ff}.abs-action-button-as-link:active,.abs-action-button-as-link:focus,.abs-action-button-as-link:hover,.action-advanced:active,.action-advanced:focus,.action-advanced:hover,.data-grid .action-delete:active,.data-grid .action-delete:focus,.data-grid .action-delete:hover{background:0 0;border:0}.abs-action-button-as-link.disabled,.abs-action-button-as-link[disabled],.action-advanced.disabled,.action-advanced[disabled],.data-grid .action-delete.disabled,.data-grid .action-delete[disabled],fieldset[disabled] .abs-action-button-as-link,fieldset[disabled] .action-advanced,fieldset[disabled] .data-grid .action-delete{color:#008bdb;opacity:.5;cursor:default;pointer-events:none;text-decoration:underline}.abs-action-button-as-link:active,.abs-action-button-as-link:not(:focus),.action-advanced:active,.action-advanced:not(:focus),.data-grid .action-delete:active,.data-grid .action-delete:not(:focus){box-shadow:none}.abs-action-button-as-link:focus,.action-advanced:focus,.data-grid .action-delete:focus{color:#0fa7ff}.abs-action-default,button{background:#e3e3e3;border-color:#adadad;color:#514943}.abs-action-default:active,.abs-action-default:focus,.abs-action-default:hover,button:active,button:focus,button:hover{background-color:#dbdbdb;color:#514943;text-decoration:none}.abs-action-primary,.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.primary,.page-actions>button.action-primary,.page-actions>button.primary,button.primary{background-color:#eb5202;border-color:#eb5202;color:#fff;text-shadow:1px 1px 0 rgba(0,0,0,.25)}.abs-action-primary:active,.abs-action-primary:focus,.abs-action-primary:hover,.page-actions .page-actions-buttons>button.action-primary:active,.page-actions .page-actions-buttons>button.action-primary:focus,.page-actions .page-actions-buttons>button.action-primary:hover,.page-actions .page-actions-buttons>button.primary:active,.page-actions .page-actions-buttons>button.primary:focus,.page-actions .page-actions-buttons>button.primary:hover,.page-actions>button.action-primary:active,.page-actions>button.action-primary:focus,.page-actions>button.action-primary:hover,.page-actions>button.primary:active,.page-actions>button.primary:focus,.page-actions>button.primary:hover,button.primary:active,button.primary:focus,button.primary:hover{background-color:#ba4000;border-color:#b84002;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.abs-action-primary.disabled,.abs-action-primary[disabled],.page-actions .page-actions-buttons>button.action-primary.disabled,.page-actions .page-actions-buttons>button.action-primary[disabled],.page-actions .page-actions-buttons>button.primary.disabled,.page-actions .page-actions-buttons>button.primary[disabled],.page-actions>button.action-primary.disabled,.page-actions>button.action-primary[disabled],.page-actions>button.primary.disabled,.page-actions>button.primary[disabled],button.primary.disabled,button.primary[disabled]{cursor:default;opacity:.5;pointer-events:none}.abs-action-secondary,.modal-popup .modal-footer .action-primary,.page-actions .page-actions-buttons>button.action-secondary,.page-actions>button.action-secondary,button.secondary{background-color:#514943;border-color:#514943;color:#fff;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.abs-action-secondary:active,.abs-action-secondary:focus,.abs-action-secondary:hover,.modal-popup .modal-footer .action-primary:active,.modal-popup .modal-footer .action-primary:focus,.modal-popup .modal-footer .action-primary:hover,.page-actions .page-actions-buttons>button.action-secondary:active,.page-actions .page-actions-buttons>button.action-secondary:focus,.page-actions .page-actions-buttons>button.action-secondary:hover,.page-actions>button.action-secondary:active,.page-actions>button.action-secondary:focus,.page-actions>button.action-secondary:hover,button.secondary:active,button.secondary:focus,button.secondary:hover{background-color:#35302c;border-color:#35302c;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.abs-action-secondary:active,.modal-popup .modal-footer .action-primary:active,.page-actions .page-actions-buttons>button.action-secondary:active,.page-actions>button.action-secondary:active,button.secondary:active{background-color:#35302c}.abs-action-tertiary,.modal-popup .modal-footer .action-secondary,button.tertiary{background-color:transparent;border-color:transparent;text-shadow:none;color:#008bdb}.abs-action-tertiary:active,.abs-action-tertiary:focus,.abs-action-tertiary:hover,.modal-popup .modal-footer .action-secondary:active,.modal-popup .modal-footer .action-secondary:focus,.modal-popup .modal-footer .action-secondary:hover,button.tertiary:active,button.tertiary:focus,button.tertiary:hover{background-color:transparent;border-color:transparent;box-shadow:none;color:#0fa7ff;text-decoration:underline}.abs-action-quaternary,.page-actions .page-actions-buttons>button,.page-actions>button{background-color:transparent;border-color:transparent;text-shadow:none;color:#333}.abs-action-quaternary:active,.abs-action-quaternary:focus,.abs-action-quaternary:hover,.page-actions .page-actions-buttons>button:active,.page-actions .page-actions-buttons>button:focus,.page-actions .page-actions-buttons>button:hover,.page-actions>button:active,.page-actions>button:focus,.page-actions>button:hover{background-color:transparent;border-color:transparent;box-shadow:none;color:#1a1a1a}.abs-action-menu,.actions-split .abs-action-menu .action-submenu,.actions-split .abs-action-menu .action-submenu .action-submenu,.actions-split .action-menu,.actions-split .action-menu .action-submenu,.actions-split .actions-split .dropdown-menu .action-submenu,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu,.actions-split .dropdown-menu{text-align:left;background-color:#fff;border:1px solid #007bdb;border-radius:1px;box-shadow:1px 1px 5px rgba(0,0,0,.5);color:#333;display:none;font-weight:400;left:0;list-style:none;margin:2px 0 0;min-width:0;padding:0;position:absolute;right:0;top:100%}.abs-action-menu._active,.actions-split .abs-action-menu .action-submenu .action-submenu._active,.actions-split .abs-action-menu .action-submenu._active,.actions-split .action-menu .action-submenu._active,.actions-split .action-menu._active,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu._active,.actions-split .actions-split .dropdown-menu .action-submenu._active,.actions-split .dropdown-menu._active{display:block}.abs-action-menu>li,.actions-split .abs-action-menu .action-submenu .action-submenu>li,.actions-split .abs-action-menu .action-submenu>li,.actions-split .action-menu .action-submenu>li,.actions-split .action-menu>li,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li,.actions-split .actions-split .dropdown-menu .action-submenu>li,.actions-split .dropdown-menu>li{border:none;display:block;padding:0;transition:background-color .1s linear}.abs-action-menu>li>a:hover,.actions-split .abs-action-menu .action-submenu .action-submenu>li>a:hover,.actions-split .abs-action-menu .action-submenu>li>a:hover,.actions-split .action-menu .action-submenu>li>a:hover,.actions-split .action-menu>li>a:hover,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li>a:hover,.actions-split .actions-split .dropdown-menu .action-submenu>li>a:hover,.actions-split .dropdown-menu>li>a:hover{text-decoration:none}.abs-action-menu>li._visible,.abs-action-menu>li:hover,.actions-split .abs-action-menu .action-submenu .action-submenu>li._visible,.actions-split .abs-action-menu .action-submenu .action-submenu>li:hover,.actions-split .abs-action-menu .action-submenu>li._visible,.actions-split .abs-action-menu .action-submenu>li:hover,.actions-split .action-menu .action-submenu>li._visible,.actions-split .action-menu .action-submenu>li:hover,.actions-split .action-menu>li._visible,.actions-split .action-menu>li:hover,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li._visible,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li:hover,.actions-split .actions-split .dropdown-menu .action-submenu>li._visible,.actions-split .actions-split .dropdown-menu .action-submenu>li:hover,.actions-split .dropdown-menu>li._visible,.actions-split .dropdown-menu>li:hover{background-color:#e3e3e3}.abs-action-menu>li:active,.actions-split .abs-action-menu .action-submenu .action-submenu>li:active,.actions-split .abs-action-menu .action-submenu>li:active,.actions-split .action-menu .action-submenu>li:active,.actions-split .action-menu>li:active,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li:active,.actions-split .actions-split .dropdown-menu .action-submenu>li:active,.actions-split .dropdown-menu>li:active{background-color:#cacaca}.abs-action-menu>li._parent,.actions-split .abs-action-menu .action-submenu .action-submenu>li._parent,.actions-split .abs-action-menu .action-submenu>li._parent,.actions-split .action-menu .action-submenu>li._parent,.actions-split .action-menu>li._parent,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li._parent,.actions-split .actions-split .dropdown-menu .action-submenu>li._parent,.actions-split .dropdown-menu>li._parent{-webkit-flex-direction:row;display:flex;-ms-flex-direction:row;flex-direction:row}.abs-action-menu>li._parent>.action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .abs-action-menu .action-submenu>li._parent>.action-menu-item,.actions-split .action-menu .action-submenu>li._parent>.action-menu-item,.actions-split .action-menu>li._parent>.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu>li._parent>.action-menu-item,.actions-split .dropdown-menu>li._parent>.action-menu-item{min-width:100%}.abs-action-menu .action-menu-item,.abs-action-menu .item,.actions-split .abs-action-menu .action-submenu .action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu .action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu .item,.actions-split .abs-action-menu .action-submenu .item,.actions-split .action-menu .action-menu-item,.actions-split .action-menu .action-submenu .action-menu-item,.actions-split .action-menu .action-submenu .item,.actions-split .action-menu .item,.actions-split .actions-split .dropdown-menu .action-submenu .action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu .action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu .item,.actions-split .actions-split .dropdown-menu .action-submenu .item,.actions-split .dropdown-menu .action-menu-item,.actions-split .dropdown-menu .item{cursor:pointer;display:block;padding:.6875em 1em}.abs-action-menu .action-submenu,.actions-split .action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu{bottom:auto;left:auto;margin-left:0;margin-top:-1px;position:absolute;right:auto;top:auto}.ie9 .abs-action-menu .action-submenu,.ie9 .actions-split .abs-action-menu .action-submenu .action-submenu,.ie9 .actions-split .abs-action-menu .action-submenu .action-submenu .action-submenu,.ie9 .actions-split .action-menu .action-submenu,.ie9 .actions-split .action-menu .action-submenu .action-submenu,.ie9 .actions-split .actions-split .dropdown-menu .action-submenu .action-submenu,.ie9 .actions-split .actions-split .dropdown-menu .action-submenu .action-submenu .action-submenu,.ie9 .actions-split .dropdown-menu .action-submenu{margin-left:99%;margin-top:-3.5rem}.abs-action-menu a.action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .abs-action-menu .action-submenu a.action-menu-item,.actions-split .action-menu .action-submenu a.action-menu-item,.actions-split .action-menu a.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu a.action-menu-item,.actions-split .dropdown-menu a.action-menu-item{color:#333}.abs-action-menu a.action-menu-item:focus,.actions-split .abs-action-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .abs-action-menu .action-submenu a.action-menu-item:focus,.actions-split .action-menu .action-submenu a.action-menu-item:focus,.actions-split .action-menu a.action-menu-item:focus,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .actions-split .dropdown-menu .action-submenu a.action-menu-item:focus,.actions-split .dropdown-menu a.action-menu-item:focus{background-color:#e3e3e3;box-shadow:none}.abs-action-wrap-triangle{position:relative}.abs-action-wrap-triangle .action-default{width:100%}.abs-action-wrap-triangle .action-default:after,.abs-action-wrap-triangle .action-default:before{border-style:solid;content:'';height:0;position:absolute;top:0;width:0}.abs-action-wrap-triangle .action-default:active,.abs-action-wrap-triangle .action-default:focus,.abs-action-wrap-triangle .action-default:hover{box-shadow:none}._keyfocus .abs-action-wrap-triangle .action-default:focus{box-shadow:0 0 0 1px #007bdb}.ie10 .abs-action-wrap-triangle .action-default.disabled,.ie10 .abs-action-wrap-triangle .action-default[disabled],.ie9 .abs-action-wrap-triangle .action-default.disabled,.ie9 .abs-action-wrap-triangle .action-default[disabled]{background-color:#fcfcfc;opacity:1;text-shadow:none}.abs-action-wrap-triangle-right{display:inline-block;padding-right:1.6rem;position:relative}.abs-action-wrap-triangle-right .action-default:after,.abs-action-wrap-triangle-right .action-default:before{border-color:transparent transparent transparent #e3e3e3;border-width:1.7rem 0 1.6rem 1.7rem;left:100%;margin-left:-1.7rem}.abs-action-wrap-triangle-right .action-default:before{border-left-color:#949494;right:-1px}.abs-action-wrap-triangle-right .action-default:active:after,.abs-action-wrap-triangle-right .action-default:focus:after,.abs-action-wrap-triangle-right .action-default:hover:after{border-left-color:#dbdbdb}.ie10 .abs-action-wrap-triangle-right .action-default.disabled:after,.ie10 .abs-action-wrap-triangle-right .action-default[disabled]:after,.ie9 .abs-action-wrap-triangle-right .action-default.disabled:after,.ie9 .abs-action-wrap-triangle-right .action-default[disabled]:after{border-color:transparent transparent transparent #fcfcfc}.abs-action-wrap-triangle-right .action-primary:after{border-color:transparent transparent transparent #eb5202}.abs-action-wrap-triangle-right .action-primary:active:after,.abs-action-wrap-triangle-right .action-primary:focus:after,.abs-action-wrap-triangle-right .action-primary:hover:after{border-left-color:#ba4000}.abs-action-wrap-triangle-left{display:inline-block;padding-left:1.6rem}.abs-action-wrap-triangle-left .action-default{text-indent:-.85rem}.abs-action-wrap-triangle-left .action-default:after,.abs-action-wrap-triangle-left .action-default:before{border-color:transparent #e3e3e3 transparent transparent;border-width:1.7rem 1.7rem 1.6rem 0;margin-right:-1.7rem;right:100%}.abs-action-wrap-triangle-left .action-default:before{border-right-color:#949494;left:-1px}.abs-action-wrap-triangle-left .action-default:active:after,.abs-action-wrap-triangle-left .action-default:focus:after,.abs-action-wrap-triangle-left .action-default:hover:after{border-right-color:#dbdbdb}.ie10 .abs-action-wrap-triangle-left .action-default.disabled:after,.ie10 .abs-action-wrap-triangle-left .action-default[disabled]:after,.ie9 .abs-action-wrap-triangle-left .action-default.disabled:after,.ie9 .abs-action-wrap-triangle-left .action-default[disabled]:after{border-color:transparent #fcfcfc transparent transparent}.abs-action-wrap-triangle-left .action-primary:after{border-color:transparent #eb5202 transparent transparent}.abs-action-wrap-triangle-left .action-primary:active:after,.abs-action-wrap-triangle-left .action-primary:focus:after,.abs-action-wrap-triangle-left .action-primary:hover:after{border-right-color:#ba4000}.action-default,button{background:#e3e3e3;border-color:#adadad;color:#514943}.action-default:active,.action-default:focus,.action-default:hover,button:active,button:focus,button:hover{background-color:#dbdbdb;color:#514943;text-decoration:none}.action-primary{background-color:#eb5202;border-color:#eb5202;color:#fff;text-shadow:1px 1px 0 rgba(0,0,0,.25)}.action-primary:active,.action-primary:focus,.action-primary:hover{background-color:#ba4000;border-color:#b84002;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.action-primary.disabled,.action-primary[disabled]{cursor:default;opacity:.5;pointer-events:none}.action-secondary{background-color:#514943;border-color:#514943;color:#fff;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.action-secondary:active,.action-secondary:focus,.action-secondary:hover{background-color:#35302c;border-color:#35302c;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.action-secondary:active{background-color:#35302c}.action-quaternary,.action-tertiary{background-color:transparent;border-color:transparent;text-shadow:none}.action-quaternary:active,.action-quaternary:focus,.action-quaternary:hover,.action-tertiary:active,.action-tertiary:focus,.action-tertiary:hover{background-color:transparent;border-color:transparent;box-shadow:none}.action-tertiary{color:#008bdb}.action-tertiary:active,.action-tertiary:focus,.action-tertiary:hover{color:#0fa7ff;text-decoration:underline}.action-quaternary{color:#333}.action-quaternary:active,.action-quaternary:focus,.action-quaternary:hover{color:#1a1a1a}.action-close>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.action-close:active{-ms-transform:scale(0.9);transform:scale(0.9)}.action-close:before{content:'\e62f';transition:color .1s linear}.action-close:hover{cursor:pointer;text-decoration:none}.abs-action-menu .action-submenu,.abs-action-menu .action-submenu .action-submenu,.action-menu,.action-menu .action-submenu,.actions-split .action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu,.actions-split .dropdown-menu .action-submenu .action-submenu{background-color:#fff;border:1px solid #007bdb;border-radius:1px;box-shadow:1px 1px 5px rgba(0,0,0,.5);color:#333;display:none;font-weight:400;left:0;list-style:none;margin:2px 0 0;min-width:0;padding:0;position:absolute;right:0;top:100%}.abs-action-menu .action-submenu .action-submenu._active,.abs-action-menu .action-submenu._active,.action-menu .action-submenu._active,.action-menu._active,.actions-split .action-menu .action-submenu .action-submenu._active,.actions-split .action-menu .action-submenu._active,.actions-split .dropdown-menu .action-submenu .action-submenu._active,.actions-split .dropdown-menu .action-submenu._active{display:block}.abs-action-menu .action-submenu .action-submenu>li,.abs-action-menu .action-submenu>li,.action-menu .action-submenu>li,.action-menu>li,.actions-split .action-menu .action-submenu .action-submenu>li,.actions-split .action-menu .action-submenu>li,.actions-split .dropdown-menu .action-submenu .action-submenu>li,.actions-split .dropdown-menu .action-submenu>li{border:none;display:block;padding:0;transition:background-color .1s linear}.abs-action-menu .action-submenu .action-submenu>li>a:hover,.abs-action-menu .action-submenu>li>a:hover,.action-menu .action-submenu>li>a:hover,.action-menu>li>a:hover,.actions-split .action-menu .action-submenu .action-submenu>li>a:hover,.actions-split .action-menu .action-submenu>li>a:hover,.actions-split .dropdown-menu .action-submenu .action-submenu>li>a:hover,.actions-split .dropdown-menu .action-submenu>li>a:hover{text-decoration:none}.abs-action-menu .action-submenu .action-submenu>li._visible,.abs-action-menu .action-submenu .action-submenu>li:hover,.abs-action-menu .action-submenu>li._visible,.abs-action-menu .action-submenu>li:hover,.action-menu .action-submenu>li._visible,.action-menu .action-submenu>li:hover,.action-menu>li._visible,.action-menu>li:hover,.actions-split .action-menu .action-submenu .action-submenu>li._visible,.actions-split .action-menu .action-submenu .action-submenu>li:hover,.actions-split .action-menu .action-submenu>li._visible,.actions-split .action-menu .action-submenu>li:hover,.actions-split .dropdown-menu .action-submenu .action-submenu>li._visible,.actions-split .dropdown-menu .action-submenu .action-submenu>li:hover,.actions-split .dropdown-menu .action-submenu>li._visible,.actions-split .dropdown-menu .action-submenu>li:hover{background-color:#e3e3e3}.abs-action-menu .action-submenu .action-submenu>li:active,.abs-action-menu .action-submenu>li:active,.action-menu .action-submenu>li:active,.action-menu>li:active,.actions-split .action-menu .action-submenu .action-submenu>li:active,.actions-split .action-menu .action-submenu>li:active,.actions-split .dropdown-menu .action-submenu .action-submenu>li:active,.actions-split .dropdown-menu .action-submenu>li:active{background-color:#cacaca}.abs-action-menu .action-submenu .action-submenu>li._parent,.abs-action-menu .action-submenu>li._parent,.action-menu .action-submenu>li._parent,.action-menu>li._parent,.actions-split .action-menu .action-submenu .action-submenu>li._parent,.actions-split .action-menu .action-submenu>li._parent,.actions-split .dropdown-menu .action-submenu .action-submenu>li._parent,.actions-split .dropdown-menu .action-submenu>li._parent{-webkit-flex-direction:row;display:flex;-ms-flex-direction:row;flex-direction:row}.abs-action-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.abs-action-menu .action-submenu>li._parent>.action-menu-item,.action-menu .action-submenu>li._parent>.action-menu-item,.action-menu>li._parent>.action-menu-item,.actions-split .action-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .action-menu .action-submenu>li._parent>.action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .dropdown-menu .action-submenu>li._parent>.action-menu-item{min-width:100%}.abs-action-menu .action-submenu .action-menu-item,.abs-action-menu .action-submenu .action-submenu .action-menu-item,.abs-action-menu .action-submenu .action-submenu .item,.abs-action-menu .action-submenu .item,.action-menu .action-menu-item,.action-menu .action-submenu .action-menu-item,.action-menu .action-submenu .item,.action-menu .item,.actions-split .action-menu .action-submenu .action-menu-item,.actions-split .action-menu .action-submenu .action-submenu .action-menu-item,.actions-split .action-menu .action-submenu .action-submenu .item,.actions-split .action-menu .action-submenu .item,.actions-split .dropdown-menu .action-submenu .action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu .action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu .item,.actions-split .dropdown-menu .action-submenu .item{cursor:pointer;display:block;padding:.6875em 1em}.abs-action-menu .action-submenu .action-submenu,.action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu .action-submenu{bottom:auto;left:auto;margin-left:0;margin-top:-1px;position:absolute;right:auto;top:auto}.ie9 .abs-action-menu .action-submenu .action-submenu,.ie9 .abs-action-menu .action-submenu .action-submenu .action-submenu,.ie9 .action-menu .action-submenu,.ie9 .action-menu .action-submenu .action-submenu,.ie9 .actions-split .action-menu .action-submenu .action-submenu,.ie9 .actions-split .action-menu .action-submenu .action-submenu .action-submenu,.ie9 .actions-split .dropdown-menu .action-submenu .action-submenu,.ie9 .actions-split .dropdown-menu .action-submenu .action-submenu .action-submenu{margin-left:99%;margin-top:-3.5rem}.abs-action-menu .action-submenu .action-submenu a.action-menu-item,.abs-action-menu .action-submenu a.action-menu-item,.action-menu .action-submenu a.action-menu-item,.action-menu a.action-menu-item,.actions-split .action-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .action-menu .action-submenu a.action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .dropdown-menu .action-submenu a.action-menu-item{color:#333}.abs-action-menu .action-submenu .action-submenu a.action-menu-item:focus,.abs-action-menu .action-submenu a.action-menu-item:focus,.action-menu .action-submenu a.action-menu-item:focus,.action-menu a.action-menu-item:focus,.actions-split .action-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .action-menu .action-submenu a.action-menu-item:focus,.actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .dropdown-menu .action-submenu a.action-menu-item:focus{background-color:#e3e3e3;box-shadow:none}.messages .message:last-child{margin:0 0 2rem}.message{background:#fffbbb;border:none;border-radius:0;color:#333;font-size:1.4rem;margin:0 0 1px;padding:1.8rem 4rem 1.8rem 5.5rem;position:relative;text-shadow:none}.message:before{background:0 0;border:0;color:#007bdb;content:'\e61a';font-family:Icons;font-size:1.9rem;font-style:normal;font-weight:400;height:auto;left:1.9rem;line-height:inherit;margin-top:-1.3rem;position:absolute;speak:none;text-shadow:none;top:50%;width:auto}.message-notice:before{color:#007bdb;content:'\e61a'}.message-warning:before{color:#eb5202;content:'\e623'}.message-error{background:#fcc}.message-error:before{color:#e22626;content:'\e632';font-size:1.5rem;left:2.2rem;margin-top:-1rem}.message-success:before{color:#79a22e;content:'\e62d'}.message-spinner:before{display:none}.message-spinner .spinner{font-size:2.5rem;left:1.5rem;position:absolute;top:1.5rem}.message-in-rating-edit{margin-left:1.8rem;margin-right:1.8rem}.modal-popup .action-close,.modal-slide .action-close{color:#736963;position:absolute;right:0;top:0;z-index:1}.modal-popup .action-close:active,.modal-slide .action-close:active{-ms-transform:none;transform:none}.modal-popup .action-close:active:before,.modal-slide .action-close:active:before{font-size:1.8rem}.modal-popup .action-close:hover:before,.modal-slide .action-close:hover:before{color:#58504b}.modal-popup .action-close:before,.modal-slide .action-close:before{font-size:2rem}.modal-popup .action-close:focus,.modal-slide .action-close:focus{background-color:transparent}.modal-popup.prompt .prompt-message{padding:2rem 0}.modal-popup.prompt .prompt-message input{width:100%}.modal-popup.confirm .modal-inner-wrap .message,.modal-popup.prompt .modal-inner-wrap .message{background:#fff}.modal-popup.modal-system-messages .modal-inner-wrap{background:#fffbbb}.modal-popup._image-box .modal-inner-wrap{margin:5rem auto;max-width:78rem;position:static}.modal-popup._image-box .thumbnail-preview{padding-bottom:3rem;text-align:center}.modal-popup._image-box .thumbnail-preview .thumbnail-preview-image-block{border:1px solid #ccc;margin:0 auto 2rem;max-width:58rem;padding:2rem}.modal-popup._image-box .thumbnail-preview .thumbnail-preview-image{max-height:54rem}.modal-popup .modal-title{font-size:2.4rem;margin-right:6.4rem}.modal-popup .modal-footer{padding-top:2.6rem;text-align:right}.modal-popup .action-close{padding:3rem}.modal-popup .action-close:active,.modal-popup .action-close:focus{background:0 0;padding-right:3.1rem;padding-top:3.1rem}.modal-slide .modal-content-new-attribute{-webkit-overflow-scrolling:touch;overflow:auto;padding-bottom:0}.modal-slide .modal-content-new-attribute iframe{margin-bottom:-2.5rem}.modal-slide .modal-title{font-size:2.1rem;margin-right:5.7rem}.modal-slide .action-close{padding:2.1rem 2.6rem}.modal-slide .action-close:active{padding-right:2.7rem;padding-top:2.2rem}.modal-slide .page-main-actions{margin-bottom:.6rem;margin-top:2.1rem}.modal-slide .magento-message{padding:0 3rem 3rem;position:relative}.modal-slide .magento-message .insert-title-inner,.modal-slide .main-col .insert-title-inner{border-bottom:1px solid #adadad;margin:0 0 2rem;padding-bottom:.5rem}.modal-slide .magento-message .insert-actions,.modal-slide .main-col .insert-actions{float:right}.modal-slide .magento-message .title,.modal-slide .main-col .title{font-size:1.6rem;padding-top:.5rem}.modal-slide .main-col,.modal-slide .side-col{float:left;padding-bottom:0}.modal-slide .main-col:after,.modal-slide .side-col:after{display:none}.modal-slide .side-col{width:20%}.modal-slide .main-col{padding-right:0;width:80%}.modal-slide .content-footer .form-buttons{float:right}.modal-title{font-weight:400;margin-bottom:0;min-height:1em}.modal-title span{font-size:1.4rem;font-style:italic;margin-left:1rem}.spinner{display:inline-block;font-size:4rem;height:1em;margin-right:1.5rem;position:relative;width:1em}.spinner>span:nth-child(1){animation-delay:.27s;-ms-transform:rotate(-315deg);transform:rotate(-315deg)}.spinner>span:nth-child(2){animation-delay:.36s;-ms-transform:rotate(-270deg);transform:rotate(-270deg)}.spinner>span:nth-child(3){animation-delay:.45s;-ms-transform:rotate(-225deg);transform:rotate(-225deg)}.spinner>span:nth-child(4){animation-delay:.54s;-ms-transform:rotate(-180deg);transform:rotate(-180deg)}.spinner>span:nth-child(5){animation-delay:.63s;-ms-transform:rotate(-135deg);transform:rotate(-135deg)}.spinner>span:nth-child(6){animation-delay:.72s;-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.spinner>span:nth-child(7){animation-delay:.81s;-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.spinner>span:nth-child(8){animation-delay:.9;-ms-transform:rotate(0deg);transform:rotate(0deg)}@keyframes fade{0%{background-color:#514943}100%{background-color:#fff}}.spinner>span{-ms-transform:scale(0.4);transform:scale(0.4);animation-name:fade;animation-duration:.72s;animation-iteration-count:infinite;animation-direction:linear;background-color:#fff;border-radius:6px;clip:rect(0 .28571429em .1em 0);height:.1em;margin-top:.5em;position:absolute;width:1em}.ie9 .spinner{background:url(../images/ajax-loader.gif) center no-repeat}.ie9 .spinner>span{display:none}.popup-loading{background:rgba(255,255,255,.8);border-color:#ef672f;color:#ef672f;font-size:14px;font-weight:700;left:50%;margin-left:-100px;padding:100px 0 10px;position:fixed;text-align:center;top:40%;width:200px;z-index:1003}.popup-loading:after{background-image:url(../images/loader-1.gif);content:'';height:64px;left:50%;margin:-32px 0 0 -32px;position:absolute;top:40%;width:64px;z-index:2}.loading-mask,.loading-old{background:rgba(255,255,255,.4);bottom:0;left:0;position:fixed;right:0;top:0;z-index:2003}.loading-mask img,.loading-old img{display:none}.loading-mask p,.loading-old p{margin-top:118px}.loading-mask .loader,.loading-old .loader{background:url(../images/loader-1.gif) 50% 30% no-repeat #f7f3eb;border-radius:5px;bottom:0;color:#575757;font-size:14px;font-weight:700;height:160px;left:0;margin:auto;opacity:.95;position:absolute;right:0;text-align:center;top:0;width:160px}.admin-user{float:right;line-height:1.36;margin-left:.3rem;z-index:490}.admin-user._active .admin__action-dropdown,.admin-user.active .admin__action-dropdown{border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.admin-user .admin__action-dropdown{height:3.3rem;padding:.7rem 2.8rem .4rem 4rem}.admin-user .admin__action-dropdown._active:after,.admin-user .admin__action-dropdown.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin-user .admin__action-dropdown:after{border-color:#777 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.3rem;top:50%;transition:all .2s linear;width:0}._active .admin-user .admin__action-dropdown:after,.active .admin-user .admin__action-dropdown:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin-user .admin__action-dropdown:hover:after{border-color:#000 transparent transparent}.admin-user .admin__action-dropdown:before{color:#777;content:'\e600';font-size:2rem;left:1.1rem;margin-top:-1.1rem;position:absolute;top:50%}.admin-user .admin__action-dropdown:hover:before{color:#333}.admin-user .admin__action-dropdown-menu{min-width:20rem;padding-left:1rem;padding-right:1rem}.admin-user .admin__action-dropdown-menu>li>a{padding-left:.5em;padding-right:1.8rem;transition:background-color .1s linear;white-space:nowrap}.admin-user .admin__action-dropdown-menu>li>a:hover{background-color:#e0f6fe;color:#333}.admin-user .admin__action-dropdown-menu>li>a:active{background-color:#c7effd;bottom:-1px;position:relative}.admin-user .admin__action-dropdown-menu .admin-user-name{text-overflow:ellipsis;white-space:nowrap;display:inline-block;max-width:20rem;overflow:hidden;vertical-align:top}.admin-user-account-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;max-width:11.2rem}.search-global{float:right;margin-right:-.3rem;position:relative;z-index:480}.search-global-field{min-width:5rem}.search-global-field._active .search-global-input{background-color:#fff;border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5);padding-right:4rem;width:25rem}.search-global-field._active .search-global-action{display:block;height:3.3rem;position:absolute;right:0;text-indent:-100%;top:0;width:5rem;z-index:3}.search-global-field .autocomplete-results{height:3.3rem;position:absolute;right:0;top:0;width:25rem}.search-global-field .search-global-menu{border:1px solid #007bdb;border-top-color:transparent;box-shadow:1px 1px 5px rgba(0,0,0,.5);left:0;margin-top:-2px;padding:0;position:absolute;right:0;top:100%;z-index:2}.search-global-field .search-global-menu:after{background-color:#fff;content:'';height:5px;left:0;position:absolute;right:0;top:-5px}.search-global-field .search-global-menu>li{background-color:#fff;border-top:1px solid #ddd;display:block;font-size:1.2rem;padding:.75rem 1.4rem .55rem}.search-global-field .search-global-menu>li._active{background-color:#e0f6fe}.search-global-field .search-global-menu .title{display:block;font-size:1.4rem}.search-global-field .search-global-menu .type{color:#1a1a1a;display:block}.search-global-label{cursor:pointer;height:3.3rem;padding:.75rem 1.4rem .55rem;position:absolute;right:0;top:0;z-index:2}.search-global-label:active{-ms-transform:scale(0.9);transform:scale(0.9)}.search-global-label:hover:before{color:#000}.search-global-label:before{color:#777;content:'\e60c';font-size:2rem}.search-global-input{background-color:transparent;border:1px solid transparent;font-size:1.4rem;height:3.3rem;padding:.75rem 1.4rem .55rem;position:absolute;right:0;top:0;transition:all .1s linear,width .3s linear;width:5rem;z-index:1}.search-global-action{display:none}.notifications-wrapper{float:right;line-height:1;position:relative}.notifications-wrapper.active{z-index:500}.notifications-wrapper.active .notifications-action{border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.notifications-wrapper.active .notifications-action:after{background-color:#fff;border:none;content:'';display:block;height:6px;left:-6px;margin-top:0;position:absolute;right:0;top:100%;width:auto}.notifications-wrapper .admin__action-dropdown-menu{padding:1rem 0 0;width:32rem}.notifications-action{color:#777;height:3.3rem;padding:.75rem 2rem .65rem}.notifications-action:after{display:none}.notifications-action:before{content:'\e607';font-size:1.9rem;margin-right:0}.notifications-action:active:before{position:relative;top:1px}.notifications-action .notifications-counter{background-color:#e22626;border-radius:1em;color:#fff;display:inline-block;font-size:1.1rem;font-weight:700;left:50%;margin-left:.3em;margin-top:-1.1em;padding:.3em .5em;position:absolute;top:50%}.notifications-entry{line-height:1.36;padding:.6rem 2rem .8rem;position:relative;transition:background-color .1s linear}.notifications-entry:hover{background-color:#e0f6fe}.notifications-entry.notifications-entry-last{margin:0 2rem;padding:.3rem 0 1.3rem;text-align:center}.notifications-entry.notifications-entry-last:hover{background-color:transparent}.notifications-entry+.notifications-entry-last{border-top:1px solid #ddd;padding-bottom:.6rem}.notifications-entry ._cutted{cursor:pointer}.notifications-entry ._cutted .notifications-entry-description-start:after{content:'...'}.notifications-entry-title{color:#ef672f;display:block;font-size:1.1rem;font-weight:700;margin-bottom:.7rem;margin-right:1em}.notifications-entry-description{color:#333;font-size:1.1rem;margin-bottom:.8rem}.notifications-entry-description-end{display:none}.notifications-entry-description-end._show{display:inline}.notifications-entry-time{color:#777;font-size:1.1rem}.notifications-close{line-height:1;padding:1rem;position:absolute;right:0;top:.6rem}.notifications-close:before{color:#ccc;content:'\e620';transition:color .1s linear}.notifications-close:hover:before{color:#b3b3b3}.notifications-close:active{-ms-transform:scale(0.95);transform:scale(0.95)}.page-header-actions{padding-top:1.1rem}.page-header-hgroup{padding-right:1.5rem}.page-title{color:#333;font-size:2.8rem}.page-header{padding:1.5rem 3rem}.menu-wrapper{display:inline-block;position:relative;width:8.8rem;z-index:700}.menu-wrapper:before{background-color:#373330;bottom:0;content:'';left:0;position:fixed;top:0;width:8.8rem;z-index:699}.menu-wrapper._fixed{left:0;position:fixed;top:0}.menu-wrapper._fixed~.page-wrapper{margin-left:8.8rem}.menu-wrapper .logo{display:block;height:8.8rem;padding:2.4rem 0 2.2rem;position:relative;text-align:center;z-index:700}._keyfocus .menu-wrapper .logo:focus{background-color:#4a4542;box-shadow:none}._keyfocus .menu-wrapper .logo:focus+.admin__menu .level-0:first-child>a{background-color:#373330}._keyfocus .menu-wrapper .logo:focus+.admin__menu .level-0:first-child>a:after{display:none}.menu-wrapper .logo:hover .logo-img{-webkit-filter:brightness(1.1);filter:brightness(1.1)}.menu-wrapper .logo:active .logo-img{-ms-transform:scale(0.95);transform:scale(0.95)}.menu-wrapper .logo .logo-img{height:4.2rem;transition:-webkit-filter .2s linear,filter .2s linear,transform .1s linear;width:3.5rem}.abs-menu-separator,.admin__menu .item-partners>a:after,.admin__menu .level-0:first-child>a:after{background-color:#736963;content:'';display:block;height:1px;left:0;margin-left:16%;position:absolute;top:0;width:68%}.admin__menu li{display:block}.admin__menu .level-0:first-child>a{position:relative}.admin__menu .level-0._active>a,.admin__menu .level-0:hover>a{color:#f7f3eb}.admin__menu .level-0._active>a{background-color:#524d49}.admin__menu .level-0:hover>a{background-color:#4a4542}.admin__menu .level-0>a{color:#aaa6a0;display:block;font-size:1rem;letter-spacing:.025em;min-height:6.2rem;padding:1.2rem .5rem .5rem;position:relative;text-align:center;text-decoration:none;text-transform:uppercase;transition:background-color .1s linear;word-wrap:break-word;z-index:700}.admin__menu .level-0>a:focus{box-shadow:none}.admin__menu .level-0>a:before{content:'\e63a';display:block;font-size:2.2rem;height:2.2rem}.admin__menu .level-0>.submenu{background-color:#4a4542;box-shadow:0 0 3px #000;left:100%;min-height:calc(8.8rem + 2rem + 100%);padding:2rem 0 0;position:absolute;top:0;-ms-transform:translateX(-100%);transform:translateX(-100%);transition-duration:.3s;transition-property:transform,visibility;transition-timing-function:ease-in-out;visibility:hidden;z-index:697}.ie10 .admin__menu .level-0>.submenu,.ie11 .admin__menu .level-0>.submenu{height:100%}.admin__menu .level-0._show>.submenu{-ms-transform:translateX(0);transform:translateX(0);visibility:visible;z-index:698}.admin__menu .level-1{margin-left:1.5rem;margin-right:1.5rem}.admin__menu [class*=level-]:not(.level-0) a{display:block;padding:1.25rem 1.5rem}.admin__menu [class*=level-]:not(.level-0) a:hover{background-color:#403934}.admin__menu [class*=level-]:not(.level-0) a:active{background-color:#322c29;padding-bottom:1.15rem;padding-top:1.35rem}.admin__menu .submenu li{min-width:23.8rem}.admin__menu .submenu a{color:#fcfcfc;transition:background-color .1s linear}.admin__menu .submenu a:focus,.admin__menu .submenu a:hover{box-shadow:none;text-decoration:none}._keyfocus .admin__menu .submenu a:focus{background-color:#403934}._keyfocus .admin__menu .submenu a:active{background-color:#322c29}.admin__menu .submenu .parent{margin-bottom:4.5rem}.admin__menu .submenu .parent .submenu-group-title{color:#a79d95;display:block;font-size:1.6rem;font-weight:600;margin-bottom:.7rem;padding:1.25rem 1.5rem;pointer-events:none}.admin__menu .submenu .column{display:table-cell}.admin__menu .submenu-title{color:#fff;display:block;font-size:2.2rem;font-weight:600;margin-bottom:4.2rem;margin-left:3rem;margin-right:5.8rem}.admin__menu .submenu-sub-title{color:#fff;display:block;font-size:1.2rem;margin:-3.8rem 5.8rem 3.8rem 3rem}.admin__menu .action-close{padding:2.4rem 2.8rem;position:absolute;right:0;top:0}.admin__menu .action-close:before{color:#a79d95;font-size:1.7rem}.admin__menu .action-close:hover:before{color:#fff}.admin__menu .item-dashboard>a:before{content:'\e604';font-size:1.8rem;padding-top:.4rem}.admin__menu .item-sales>a:before{content:'\e60b'}.admin__menu .item-catalog>a:before{content:'\e608'}.admin__menu .item-customer>a:before{content:'\e603';font-size:2.6rem;position:relative;top:-.4rem}.admin__menu .item-marketing>a:before{content:'\e609';font-size:2rem;padding-top:.2rem}.admin__menu .item-content>a:before{content:'\e602';font-size:2.4rem;position:relative;top:-.2rem}.admin__menu .item-report>a:before{content:'\e60a'}.admin__menu .item-stores>a:before{content:'\e60d';font-size:1.9rem;padding-top:.3rem}.admin__menu .item-system>a:before{content:'\e610'}.admin__menu .item-partners._active>a:after,.admin__menu .item-system._current+.item-partners>a:after{display:none}.admin__menu .item-partners>a{padding-bottom:1rem}.admin__menu .item-partners>a:before{content:'\e612'}.admin__menu .level-0>.submenu>ul>.level-1:only-of-type>.submenu-group-title,.admin__menu .submenu .column:only-of-type .submenu-group-title{display:none}.admin__menu-overlay{bottom:0;left:0;position:fixed;right:0;top:0;z-index:697}.store-switcher{color:#333;float:left;font-size:1.3rem;margin-top:.7rem}.store-switcher .admin__action-dropdown{background-color:#f8f8f8;margin-left:.5em}.store-switcher .dropdown{display:inline-block;position:relative}.store-switcher .dropdown:after,.store-switcher .dropdown:before{content:'';display:table}.store-switcher .dropdown:after{clear:both}.store-switcher .dropdown .action.toggle{cursor:pointer;display:inline-block;text-decoration:none}.store-switcher .dropdown .action.toggle:after{-webkit-font-smoothing:antialiased;font-size:22px;line-height:2;color:#333;content:'\e607';font-family:icons-blank-theme;margin:0;vertical-align:top;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.store-switcher .dropdown .action.toggle:active:after,.store-switcher .dropdown .action.toggle:hover:after{color:#333}.store-switcher .dropdown .action.toggle.active{display:inline-block;text-decoration:none}.store-switcher .dropdown .action.toggle.active:after{-webkit-font-smoothing:antialiased;font-size:22px;line-height:2;color:#333;content:'\e618';font-family:icons-blank-theme;margin:0;vertical-align:top;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.store-switcher .dropdown .action.toggle.active:active:after,.store-switcher .dropdown .action.toggle.active:hover:after{color:#333}.store-switcher .dropdown .dropdown-menu{margin:4px 0 0;padding:0;list-style:none;background:#fff;border:1px solid #aaa6a0;min-width:19.5rem;z-index:100;box-sizing:border-box;display:none;position:absolute;top:100%;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.store-switcher .dropdown .dropdown-menu li{margin:0;padding:0}.store-switcher .dropdown .dropdown-menu li:hover{background:0 0;cursor:pointer}.store-switcher .dropdown.active{overflow:visible}.store-switcher .dropdown.active .dropdown-menu{display:block}.store-switcher .dropdown-menu{left:0;margin-top:.5em;max-height:250px;overflow-y:auto;padding-top:.25em}.store-switcher .dropdown-menu li{border:0;cursor:default}.store-switcher .dropdown-menu li:hover{cursor:default}.store-switcher .dropdown-menu li a,.store-switcher .dropdown-menu li span{color:#333;display:block;padding:.5rem 1.3rem}.store-switcher .dropdown-menu li a{text-decoration:none}.store-switcher .dropdown-menu li a:hover{background:#e9e9e9}.store-switcher .dropdown-menu li span{color:#adadad;cursor:default}.store-switcher .dropdown-menu li.current span{background:#eee;color:#333}.store-switcher .dropdown-menu .store-switcher-store a,.store-switcher .dropdown-menu .store-switcher-store span{padding-left:2.6rem}.store-switcher .dropdown-menu .store-switcher-store-view a,.store-switcher .dropdown-menu .store-switcher-store-view span{padding-left:3.9rem}.store-switcher .dropdown-menu .dropdown-toolbar{border-top:1px solid #ebebeb;margin-top:1rem}.store-switcher .dropdown-menu .dropdown-toolbar a:before{content:'\e610';margin-right:.25em;position:relative;top:1px}.store-switcher-label{font-weight:700}.store-switcher-alt{display:inline-block;position:relative}.store-switcher-alt.active .dropdown-menu{display:block}.store-switcher-alt .dropdown-menu{margin-top:2px;white-space:nowrap}.store-switcher-alt .dropdown-menu ul{list-style:none;margin:0;padding:0}.store-switcher-alt strong{color:#a79d95;display:block;font-size:14px;font-weight:500;line-height:1.333;padding:5px 10px}.store-switcher-alt .store-selected{color:#676056;cursor:pointer;font-size:12px;font-weight:400;line-height:1.333}.store-switcher-alt .store-selected:after{-webkit-font-smoothing:antialiased;color:#afadac;content:'\e02c';font-style:normal;font-weight:400;margin:0 0 0 3px;speak:none;vertical-align:text-top}.store-switcher-alt .store-switcher-store,.store-switcher-alt .store-switcher-website{padding:0}.store-switcher-alt .store-switcher-store:hover,.store-switcher-alt .store-switcher-website:hover{background:0 0}.store-switcher-alt .manage-stores,.store-switcher-alt .store-switcher-all,.store-switcher-alt .store-switcher-store-view{padding:0}.store-switcher-alt .manage-stores>a,.store-switcher-alt .store-switcher-all>a{color:#676056;display:block;font-size:12px;padding:8px 15px;text-decoration:none}.store-switcher-website{margin:5px 0 0}.store-switcher-website>strong{padding-left:13px}.store-switcher-store{margin:1px 0 0}.store-switcher-store>strong{padding-left:20px}.store-switcher-store>ul{margin-top:1px}.store-switcher-store-view:first-child{border-top:1px solid #e5e5e5}.store-switcher-store-view>a{color:#333;display:block;font-size:13px;padding:5px 15px 5px 24px;text-decoration:none}.store-view:not(.store-switcher){float:left}.store-view .store-switcher-label{display:inline-block;margin-top:1rem}.tooltip{margin-left:.5em}.tooltip .help a,.tooltip .help span{cursor:pointer;display:inline-block;height:22px;position:relative;vertical-align:middle;width:22px;z-index:2}.tooltip .help a:before,.tooltip .help span:before{color:#333;content:'\e633';font-size:1.7rem}.tooltip .help a:hover{text-decoration:none}.tooltip .tooltip-content{background:#000;border-radius:3px;color:#fff;display:none;margin-left:-19px;margin-top:10px;max-width:200px;padding:4px 8px;position:absolute;text-shadow:none;z-index:20}.tooltip .tooltip-content:before{border-bottom:5px solid #000;border-left:5px solid transparent;border-right:5px solid transparent;content:'';height:0;left:20px;opacity:.8;position:absolute;top:-5px;width:0}.tooltip .tooltip-content.loading{position:absolute}.tooltip .tooltip-content.loading:before{border-bottom-color:rgba(0,0,0,.3)}.tooltip:hover>.tooltip-content{display:block}.page-actions._fixed,.page-main-actions:not(._hidden){background:#f8f8f8;border-bottom:1px solid #e3e3e3;border-top:1px solid #e3e3e3;padding:1.5rem}.page-main-actions{margin:0 0 3rem}.page-main-actions._hidden .store-switcher{display:none}.page-main-actions._hidden .page-actions-placeholder{min-height:50px}.page-actions{float:right}.page-main-actions .page-actions._fixed{left:8.8rem;position:fixed;right:0;top:0;z-index:501}.page-main-actions .page-actions._fixed .page-actions-inner:before{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#333;content:attr(data-title);float:left;font-size:2.8rem;margin-top:.3rem;max-width:50%}.page-actions .page-actions-buttons>button,.page-actions>button{float:right;margin-left:1.3rem}.page-actions .page-actions-buttons>button.action-back,.page-actions .page-actions-buttons>button.back,.page-actions>button.action-back,.page-actions>button.back{float:left;-ms-flex-order:-1;order:-1}.page-actions .page-actions-buttons>button.action-back:before,.page-actions .page-actions-buttons>button.back:before,.page-actions>button.action-back:before,.page-actions>button.back:before{content:'\e626';margin-right:.5em;position:relative;top:1px}.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.primary,.page-actions>button.action-primary,.page-actions>button.primary{-ms-flex-order:2;order:2}.page-actions .page-actions-buttons>button.save:not(.primary),.page-actions>button.save:not(.primary){-ms-flex-order:1;order:1}.page-actions .page-actions-buttons>button.delete,.page-actions>button.delete{-ms-flex-order:-1;order:-1}.page-actions .actions-split{float:right;margin-left:1.3rem;-ms-flex-order:2;order:2}.page-actions .actions-split .dropdown-menu .item{display:block}.page-actions-buttons{float:right;-ms-flex-pack:end;justify-content:flex-end;display:-ms-flexbox;display:flex}.customer-index-edit .page-actions-buttons{background-color:transparent}.admin__page-nav{background:#f1f1f1;border:1px solid #e3e3e3}.admin__page-nav._collapsed:first-child{border-bottom:none}.admin__page-nav._collapsed._show{border-bottom:1px solid #e3e3e3}.admin__page-nav._collapsed._show ._collapsible{background:#f1f1f1}.admin__page-nav._collapsed._show ._collapsible:after{content:'\e62b'}.admin__page-nav._collapsed._show ._collapsible+.admin__page-nav-items{display:block}.admin__page-nav._collapsed._hide .admin__page-nav-title-messages,.admin__page-nav._collapsed._hide .admin__page-nav-title-messages ._active{display:inline-block}.admin__page-nav+._collapsed{border-bottom:none;border-top:none}.admin__page-nav-title{border-bottom:1px solid #e3e3e3;color:#303030;display:block;font-size:1.4rem;line-height:1.2;margin:0 0 -1px;padding:1.8rem 1.5rem;position:relative;text-transform:uppercase}.admin__page-nav-title._collapsible{background:#fff;cursor:pointer;margin:0;padding-right:3.5rem;transition:border-color .1s ease-out,background-color .1s ease-out}.admin__page-nav-title._collapsible+.admin__page-nav-items{display:none;margin-top:-1px}.admin__page-nav-title._collapsible:after{content:'\e628';font-size:1.3rem;font-weight:700;position:absolute;right:1.8rem;top:2rem}.admin__page-nav-title._collapsible:hover{background:#f1f1f1}.admin__page-nav-title._collapsible:last-child{margin:0 0 -1px}.admin__page-nav-title strong{font-weight:700}.admin__page-nav-title .admin__page-nav-title-messages{display:none}.admin__page-nav-items{list-style-type:none;margin:0;padding:1rem 0 1.3rem}.admin__page-nav-item{border-left:3px solid transparent;margin-left:.7rem;padding:0;position:relative;transition:border-color .1s ease-out,background-color .1s ease-out}.admin__page-nav-item:hover{border-color:#e4e4e4}.admin__page-nav-item:hover .admin__page-nav-link{background:#e4e4e4;color:#303030;text-decoration:none}.admin__page-nav-item._active,.admin__page-nav-item.ui-state-active{border-color:#eb5202}.admin__page-nav-item._active .admin__page-nav-link,.admin__page-nav-item.ui-state-active .admin__page-nav-link{background:#fff;border-color:#e3e3e3;border-right:1px solid #fff;color:#303030;margin-right:-1px;font-weight:600}.admin__page-nav-item._loading:before,.admin__page-nav-item.ui-tabs-loading:before{display:none}.admin__page-nav-item._loading .admin__page-nav-item-message-loader,.admin__page-nav-item.ui-tabs-loading .admin__page-nav-item-message-loader{display:inline-block}.admin__page-nav-link{border:1px solid transparent;border-width:1px 0;color:#303030;display:block;font-weight:500;line-height:1.2;margin:0 0 -1px;padding:2rem 4rem 2rem 1rem;transition:border-color .1s ease-out,background-color .1s ease-out;word-wrap:break-word}.admin__page-nav-item-messages{display:inline-block}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip{background:#f1f1f1;border:1px solid #f1f1f1;border-radius:1px;bottom:3.7rem;box-shadow:0 3px 9px 0 rgba(0,0,0,.3);display:none;font-size:1.4rem;font-weight:400;left:-1rem;line-height:1.36;padding:1.5rem;position:absolute;text-transform:none;width:27rem;word-break:normal;z-index:2}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:after,.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:before{border:15px solid transparent;height:0;width:0;border-top-color:#f1f1f1;content:'';display:block;left:2rem;position:absolute;top:100%;z-index:3}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:after{border-top-color:#f1f1f1;margin-top:-1px;z-index:4}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:before{border-top-color:#bfbfbf;margin-top:1px}.admin__page-nav-item-message-loader{display:none;margin-top:-1rem;position:absolute;right:0;top:50%}.admin__page-nav-item-message-loader .spinner{font-size:2rem;margin-right:1.5rem}._loading>.admin__page-nav-item-messages .admin__page-nav-item-message-loader{display:inline-block}.admin__page-nav-item-message{position:relative}.admin__page-nav-item-message:hover{z-index:500}.admin__page-nav-item-message:hover .admin__page-nav-item-message-tooltip{display:block}.admin__page-nav-item-message._changed,.admin__page-nav-item-message._error{display:none}.admin__page-nav-item-message .admin__page-nav-item-message-icon{display:inline-block;font-size:1.4rem;padding-left:.8em;vertical-align:baseline}.admin__page-nav-item-message .admin__page-nav-item-message-icon:after{color:#666;content:'\e631'}._changed:not(._error)>.admin__page-nav-item-messages ._changed{display:inline-block}._error .admin__page-nav-item-message-icon:after{color:#eb5202;content:'\e623'}._error>.admin__page-nav-item-messages ._error{display:inline-block}._error>.admin__page-nav-item-messages ._error .spinner{font-size:2rem;margin-right:1.5rem}._error .admin__page-nav-item-message-tooltip{background:#f1f1f1;border:1px solid #f1f1f1;border-radius:1px;bottom:3.7rem;box-shadow:0 3px 9px 0 rgba(0,0,0,.3);display:none;font-weight:400;left:-1rem;line-height:1.36;padding:2rem;position:absolute;text-transform:none;width:27rem;word-break:normal;z-index:2}._error .admin__page-nav-item-message-tooltip:after,._error .admin__page-nav-item-message-tooltip:before{border:15px solid transparent;height:0;width:0;border-top-color:#f1f1f1;content:'';display:block;left:2rem;position:absolute;top:100%;z-index:3}._error .admin__page-nav-item-message-tooltip:after{border-top-color:#f1f1f1;margin-top:-1px;z-index:4}._error .admin__page-nav-item-message-tooltip:before{border-top-color:#bfbfbf}.admin__data-grid-wrap-static .data-grid{box-sizing:border-box}.admin__data-grid-wrap-static .data-grid thead{color:#333}.admin__data-grid-wrap-static .data-grid tr:nth-child(even) td{background-color:#f5f5f5}.admin__data-grid-wrap-static .data-grid tr:nth-child(even) td._dragging{background-color:rgba(245,245,245,.95)}.admin__data-grid-wrap-static .data-grid ul{margin-left:1rem;padding-left:1rem}.admin__data-grid-wrap-static .admin__data-grid-loading-mask{background:rgba(255,255,255,.5);bottom:0;left:0;position:absolute;right:0;top:0;z-index:399}.admin__data-grid-wrap-static .admin__data-grid-loading-mask .grid-loader{background:url(../images/loader-2.gif) 50% 50% no-repeat;bottom:0;height:149px;left:0;margin:auto;position:absolute;right:0;top:0;width:218px}.data-grid-filters-actions-wrap{float:right}.data-grid-search-control-wrap{float:left;max-width:45.5rem;position:relative;width:35%}.data-grid-search-control-wrap :-ms-input-placeholder{font-style:italic}.data-grid-search-control-wrap ::-webkit-input-placeholder{font-style:italic}.data-grid-search-control-wrap ::-moz-placeholder{font-style:italic}.data-grid-search-control-wrap .action-submit{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;padding:.6rem 2rem .2rem;position:absolute;right:0;top:1px}.data-grid-search-control-wrap .action-submit:hover{background-color:transparent;border:none;box-shadow:none}.data-grid-search-control-wrap .action-submit:active{-ms-transform:scale(0.9);transform:scale(0.9)}.data-grid-search-control-wrap .action-submit:hover:before{color:#1a1a1a}._keyfocus .data-grid-search-control-wrap .action-submit:focus{box-shadow:0 0 0 1px #008bdb}.data-grid-search-control-wrap .action-submit:before{content:'\e60c';font-size:2rem;transition:color .1s linear}.data-grid-search-control-wrap .action-submit>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.data-grid-search-control-wrap .abs-action-menu .action-submenu,.data-grid-search-control-wrap .abs-action-menu .action-submenu .action-submenu,.data-grid-search-control-wrap .action-menu,.data-grid-search-control-wrap .action-menu .action-submenu,.data-grid-search-control-wrap .actions-split .action-menu .action-submenu,.data-grid-search-control-wrap .actions-split .action-menu .action-submenu .action-submenu,.data-grid-search-control-wrap .actions-split .dropdown-menu .action-submenu,.data-grid-search-control-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{max-height:19.25rem;overflow-y:auto;z-index:398}.data-grid-search-control-wrap .action-menu-item._selected{background-color:#e0f6fe}.data-grid-search-control-wrap .data-grid-search-label{display:none}.data-grid-search-control{padding-right:6rem;width:100%}.data-grid-filters-action-wrap{float:left;padding-left:2rem}.data-grid-filters-action-wrap .action-default{font-size:1.3rem;margin-bottom:1rem;padding-left:1.7rem;padding-right:2.1rem;padding-top:.7rem}.data-grid-filters-action-wrap .action-default._active{background-color:#fff;border-bottom-color:#fff;border-right-color:#ccc;font-weight:600;margin:-.1rem 0 0;padding-bottom:1.6rem;padding-top:.8rem;position:relative;z-index:281}.data-grid-filters-action-wrap .action-default._active:after{background-color:#eb5202;bottom:100%;content:'';height:3px;left:-1px;position:absolute;right:-1px}.data-grid-filters-action-wrap .action-default:before{color:#333;content:'\e605';font-size:1.8rem;margin-right:.4rem;position:relative;top:-1px;vertical-align:top}.data-grid-filters-action-wrap .filters-active{display:none}.admin__action-grid-select .admin__control-select{margin:-.5rem .5rem 0 0;padding-bottom:.6rem;padding-top:.6rem}.admin__data-grid-filters-wrap{opacity:0;visibility:hidden;clear:both;font-size:1.3rem;transition:opacity .3s ease}.admin__data-grid-filters-wrap._show{opacity:1;visibility:visible;border-bottom:1px solid #ccc;border-top:1px solid #ccc;margin-bottom:.7rem;padding:3.6rem 0 3rem;position:relative;top:-1px;z-index:280}.admin__data-grid-filters-wrap._show .admin__data-grid-filters,.admin__data-grid-filters-wrap._show .admin__data-grid-filters-footer{display:block}.admin__data-grid-filters-wrap .admin__form-field-label,.admin__data-grid-filters-wrap .admin__form-field-legend{display:block;font-weight:700;margin:0 0 .3rem;text-align:left}.admin__data-grid-filters-wrap .admin__form-field{display:inline-block;margin-bottom:2em;margin-left:0;padding-left:2rem;padding-right:2rem;vertical-align:top;width:calc(100% / 4 - 4px)}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field{display:block;float:none;margin-bottom:1.5rem;padding-left:0;padding-right:0;width:auto}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field:last-child{margin-bottom:0}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field .admin__form-field-label{border:1px solid transparent;float:left;font-weight:400;line-height:1.36;margin-bottom:0;padding-bottom:.6rem;padding-right:1em;padding-top:.6rem;width:25%}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field .admin__form-field-control{margin-left:25%}.admin__data-grid-filters-wrap .admin__action-multiselect,.admin__data-grid-filters-wrap .admin__control-select,.admin__data-grid-filters-wrap .admin__control-text,.admin__data-grid-filters-wrap .admin__form-field-label{font-size:1.3rem}.admin__data-grid-filters-wrap .admin__control-select{height:3.2rem;padding-top:.5rem}.admin__data-grid-filters-wrap .admin__action-multiselect:before{height:3.2rem;width:3.2rem}.admin__data-grid-filters-wrap .admin__control-select,.admin__data-grid-filters-wrap .admin__control-text._has-datepicker{width:100%}.admin__data-grid-filters{display:none;margin-left:-2rem;margin-right:-2rem}.admin__filters-legend{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.admin__data-grid-filters-footer{display:none;font-size:1.4rem}.admin__data-grid-filters-footer .admin__footer-main-actions{margin-left:25%;text-align:right}.admin__data-grid-filters-footer .admin__footer-secondary-actions{float:left;width:50%}.admin__data-grid-filters-current{border-bottom:.1rem solid #ccc;border-top:.1rem solid #ccc;display:none;font-size:1.3rem;margin-bottom:.9rem;padding-bottom:.8rem;padding-top:1.1rem;width:100%}.admin__data-grid-filters-current._show{display:table;position:relative;top:-1px;z-index:3}.admin__data-grid-filters-current._show+.admin__data-grid-filters-wrap._show{margin-top:-1rem}.admin__current-filters-actions-wrap,.admin__current-filters-list-wrap,.admin__current-filters-title-wrap{display:table-cell;vertical-align:top}.admin__current-filters-title{margin-right:1em;white-space:nowrap}.admin__current-filters-list-wrap{width:100%}.admin__current-filters-list{margin-bottom:0}.admin__current-filters-list>li{display:inline-block;font-weight:600;margin:0 1rem .5rem;padding-right:2.6rem;position:relative}.admin__current-filters-list .action-remove{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;padding:0;line-height:1;position:absolute;right:0;top:1px}.admin__current-filters-list .action-remove:hover{background-color:transparent;border:none;box-shadow:none}.admin__current-filters-list .action-remove:hover:before{color:#949494}.admin__current-filters-list .action-remove:active{-ms-transform:scale(0.9);transform:scale(0.9)}.admin__current-filters-list .action-remove:before{color:#adadad;content:'\e620';font-size:1.6rem;transition:color .1s linear}.admin__current-filters-list .action-remove>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.admin__current-filters-actions-wrap .action-clear{border:none;padding-bottom:0;padding-top:0;white-space:nowrap}.admin__data-grid-pager-wrap{float:right;text-align:right}.admin__data-grid-pager{display:inline-block;margin-left:3rem}.admin__data-grid-pager .admin__control-text::-webkit-inner-spin-button,.admin__data-grid-pager .admin__control-text::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.admin__data-grid-pager .admin__control-text{-moz-appearance:textfield;text-align:center;width:4.4rem}.action-next,.action-previous{width:4.4rem}.action-next:before,.action-previous:before{font-weight:700}.action-next>span,.action-previous>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.action-previous{margin-right:2.5rem;text-indent:-.25em}.action-previous:before{content:'\e629'}.action-next{margin-left:1.5rem;text-indent:.1em}.action-next:before{content:'\e62a'}.admin__data-grid-action-bookmarks{opacity:.98}.admin__data-grid-action-bookmarks .admin__action-dropdown-text:after{left:0;right:-6px}.admin__data-grid-action-bookmarks._active{z-index:290}.admin__data-grid-action-bookmarks .admin__action-dropdown .admin__action-dropdown-text{display:inline-block;max-width:15rem;min-width:4.9rem;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.admin__data-grid-action-bookmarks .admin__action-dropdown:before{content:'\e60f'}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu{font-size:1.3rem;left:0;padding:1rem 0;right:auto}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu>li{padding:0 5rem 0 0;position:relative;white-space:nowrap}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu>li:not(.action-dropdown-menu-action){transition:background-color .1s linear}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu>li:not(.action-dropdown-menu-action):hover{background-color:#e3e3e3}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item{max-width:23rem;min-width:18rem;white-space:normal;word-break:break-all}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-edit{display:none;padding-bottom:1rem;padding-left:1rem;padding-top:1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-edit .action-dropdown-menu-item-actions{padding-bottom:1rem;padding-top:1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action{padding-left:1rem;padding-top:1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action+.action-dropdown-menu-item-last{padding-top:.5rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action>a{color:#008bdb;text-decoration:none;display:inline-block;padding-left:1.1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action>a:hover{color:#0fa7ff;text-decoration:underline}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-last{padding-bottom:0}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu ._edit .action-dropdown-menu-item{display:none}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu ._edit .action-dropdown-menu-item-edit{display:block}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu ._active .action-dropdown-menu-link{font-weight:600}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .admin__control-text{font-size:1.3rem;min-width:15rem;width:calc(100% - 4rem)}.ie9 .admin__data-grid-action-bookmarks .admin__action-dropdown-menu .admin__control-text{width:15rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-actions{border-left:1px solid #fff;bottom:0;position:absolute;right:0;top:0;width:5rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-link{color:#333;display:block;text-decoration:none;padding:1rem 1rem 1rem 2.1rem}.admin__data-grid-action-bookmarks .action-delete,.admin__data-grid-action-bookmarks .action-edit,.admin__data-grid-action-bookmarks .action-submit{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;vertical-align:top}.admin__data-grid-action-bookmarks .action-delete:hover,.admin__data-grid-action-bookmarks .action-edit:hover,.admin__data-grid-action-bookmarks .action-submit:hover{background-color:transparent;border:none;box-shadow:none}.admin__data-grid-action-bookmarks .action-delete:before,.admin__data-grid-action-bookmarks .action-edit:before,.admin__data-grid-action-bookmarks .action-submit:before{font-size:1.7rem}.admin__data-grid-action-bookmarks .action-delete>span,.admin__data-grid-action-bookmarks .action-edit>span,.admin__data-grid-action-bookmarks .action-submit>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.admin__data-grid-action-bookmarks .action-delete,.admin__data-grid-action-bookmarks .action-edit{padding:.6rem 1.4rem}.admin__data-grid-action-bookmarks .action-delete:active,.admin__data-grid-action-bookmarks .action-edit:active{-ms-transform:scale(0.9);transform:scale(0.9)}.admin__data-grid-action-bookmarks .action-submit{padding:.6rem 1rem .6rem .8rem}.admin__data-grid-action-bookmarks .action-submit:active{position:relative;right:-1px}.admin__data-grid-action-bookmarks .action-submit:before{content:'\e625'}.admin__data-grid-action-bookmarks .action-delete:before{content:'\e630'}.admin__data-grid-action-bookmarks .action-edit{padding-top:.8rem}.admin__data-grid-action-bookmarks .action-edit:before{content:'\e631'}.admin__data-grid-action-columns._active{opacity:.98;z-index:290}.admin__data-grid-action-columns .admin__action-dropdown:before{content:'\e610';font-size:1.8rem;margin-right:.7rem;vertical-align:top}.admin__data-grid-action-columns-menu{color:#303030;font-size:1.3rem;overflow:hidden;padding:2.2rem 3.5rem 1rem;z-index:1}.admin__data-grid-action-columns-menu._overflow .admin__action-dropdown-menu-header{border-bottom:1px solid #d1d1d1}.admin__data-grid-action-columns-menu._overflow .admin__action-dropdown-menu-content{width:49.2rem}.admin__data-grid-action-columns-menu._overflow .admin__action-dropdown-menu-footer{border-top:1px solid #d1d1d1;padding-top:2.5rem}.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content{max-height:22.85rem;overflow-y:auto;padding-top:1.5rem;position:relative;width:47.4rem}.admin__data-grid-action-columns-menu .admin__field-option{float:left;height:1.9rem;margin-bottom:1.5rem;padding:0 1rem 0 0;width:15.8rem}.admin__data-grid-action-columns-menu .admin__field-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-header{padding-bottom:1.5rem}.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-footer{padding:1rem 0 2rem}.admin__data-grid-action-columns-menu .admin__action-dropdown-footer-main-actions{margin-left:25%;text-align:right}.admin__data-grid-action-columns-menu .admin__action-dropdown-footer-secondary-actions{float:left;margin-left:-1em}.admin__data-grid-action-export._active{opacity:.98;z-index:290}.admin__data-grid-action-export .admin__action-dropdown:before{content:'\e635';font-size:1.7rem;left:.3rem;margin-right:.7rem;vertical-align:top}.admin__data-grid-action-export-menu{padding-left:2rem;padding-right:2rem;padding-top:1rem}.admin__data-grid-action-export-menu .admin__action-dropdown-footer-main-actions{padding-bottom:2rem;padding-top:2.5rem;white-space:nowrap}.sticky-header{background-color:#f8f8f8;border-bottom:1px solid #e3e3e3;box-shadow:0 5px 5px 0 rgba(0,0,0,.25);left:8.8rem;margin-top:-1px;padding:.5rem 3rem 0;position:fixed;right:0;top:77px;z-index:398}.sticky-header .admin__data-grid-wrap{margin-bottom:0;overflow-x:visible;padding-bottom:0}.sticky-header .admin__data-grid-header-row{position:relative;text-align:right}.sticky-header .admin__data-grid-header-row:last-child{margin:0}.sticky-header .admin__data-grid-actions-wrap,.sticky-header .admin__data-grid-filters-wrap,.sticky-header .admin__data-grid-pager-wrap,.sticky-header .data-grid-filters-actions-wrap,.sticky-header .data-grid-search-control-wrap{display:inline-block;float:none;vertical-align:top}.sticky-header .action-select-wrap{float:left;margin-right:1.5rem;width:16.66666667%}.sticky-header .admin__control-support-text{float:left}.sticky-header .data-grid-search-control-wrap{margin:-.5rem 0 0 1.1rem;width:auto}.sticky-header .data-grid-search-control-wrap .data-grid-search-label{box-sizing:border-box;cursor:pointer;display:block;min-width:3.8rem;padding:1.2rem .6rem 1.7rem;position:relative;text-align:center}.sticky-header .data-grid-search-control-wrap .data-grid-search-label:before{color:#333;content:'\e60c';font-size:2rem;transition:color .1s linear}.sticky-header .data-grid-search-control-wrap .data-grid-search-label:hover:before{color:#000}.sticky-header .data-grid-search-control-wrap .data-grid-search-label span{display:none}.sticky-header .data-grid-filters-actions-wrap{margin:-.5rem 0 0 1.1rem;padding-left:0;position:relative}.sticky-header .data-grid-filters-actions-wrap .action-default{background-color:transparent;border:1px solid transparent;box-sizing:border-box;min-width:3.8rem;padding:1.2rem .6rem 1.7rem;text-align:center;transition:all .15s ease}.sticky-header .data-grid-filters-actions-wrap .action-default span{display:none}.sticky-header .data-grid-filters-actions-wrap .action-default:before{margin:0}.sticky-header .data-grid-filters-actions-wrap .action-default._active{background-color:#fff;border-color:#adadad #adadad #fff;box-shadow:1px 1px 5px rgba(0,0,0,.5);z-index:210}.sticky-header .data-grid-filters-actions-wrap .action-default._active:after{background-color:#fff;content:'';height:6px;left:-2px;position:absolute;right:-6px;top:100%}.sticky-header .data-grid-filters-action-wrap{padding:0}.sticky-header .admin__data-grid-filters-wrap{background-color:#fff;border:1px solid #adadad;box-shadow:0 5px 5px 0 rgba(0,0,0,.25);left:0;padding-left:3.5rem;padding-right:3.5rem;position:absolute;top:100%;width:100%;z-index:209}.sticky-header .admin__data-grid-filters-current+.admin__data-grid-filters-wrap._show{margin-top:-6px}.sticky-header .filters-active{background-color:#e04f00;border-radius:10px;color:#fff;display:block;font-size:1.4rem;font-weight:700;padding:.1rem .7rem;position:absolute;right:-7px;top:0;z-index:211}.sticky-header .filters-active:empty{padding-bottom:0;padding-top:0}.sticky-header .admin__data-grid-actions-wrap{margin:-.5rem 0 0 1.1rem;padding-right:.3rem}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown{background-color:transparent;box-sizing:border-box;min-width:3.8rem;padding-left:.6rem;padding-right:.6rem;text-align:center}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown .admin__action-dropdown-text{display:inline-block;max-width:0;min-width:0;overflow:hidden}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown:before{margin:0}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown-wrap{margin-right:1.1rem}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown-wrap:after,.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown:after{display:none}.sticky-header .admin__data-grid-actions-wrap ._active .admin__action-dropdown{background-color:#fff}.sticky-header .admin__data-grid-action-bookmarks .admin__action-dropdown:before{position:relative;top:-3px}.sticky-header .admin__data-grid-filters-current{border-bottom:0;border-top:0;margin-bottom:0;padding-bottom:0;padding-top:0}.sticky-header .admin__data-grid-pager .admin__control-text,.sticky-header .admin__data-grid-pager-wrap .admin__control-support-text,.sticky-header .data-grid-search-control-wrap .action-submit,.sticky-header .data-grid-search-control-wrap .data-grid-search-control{display:none}.sticky-header .action-next{margin:0}.sticky-header .data-grid{margin-bottom:-1px}.data-grid-cap-left,.data-grid-cap-right{background-color:#f8f8f8;bottom:-2px;position:absolute;top:6rem;width:3rem;z-index:201}.data-grid-cap-left{left:0}.admin__data-grid-header{font-size:1.4rem}.admin__data-grid-header-row+.admin__data-grid-header-row{margin-top:1.1rem}.admin__data-grid-header-row:last-child{margin-bottom:0}.admin__data-grid-header-row .action-select-wrap{display:block}.admin__data-grid-header-row .action-select{width:100%}.admin__data-grid-actions-wrap{float:right;margin-left:1.1rem;margin-top:-.5rem;text-align:right}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap{position:relative;text-align:left;vertical-align:middle}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active+.admin__action-dropdown-wrap:after,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active:after,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._hide+.admin__action-dropdown-wrap:after,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap:first-child:after{display:none}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active .admin__action-dropdown,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active .admin__action-dropdown-menu{border-color:#adadad}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap:after{border-left:1px solid #ccc;content:'';height:3.2rem;left:0;position:absolute;top:.5rem;z-index:3}.admin__data-grid-actions-wrap .admin__action-dropdown{padding-bottom:1.7rem;padding-top:1.2rem}.admin__data-grid-actions-wrap .admin__action-dropdown:after{margin-top:-.4rem}.admin__data-grid-outer-wrap{min-height:8rem;position:relative}.admin__data-grid-wrap{margin-bottom:2rem;max-width:100%;overflow-x:auto;padding-bottom:1rem;padding-top:2rem}.admin__data-grid-loading-mask{background:rgba(255,255,255,.5);bottom:0;left:0;position:absolute;right:0;top:0;z-index:399}.admin__data-grid-loading-mask .spinner{font-size:4rem;left:50%;margin-left:-2rem;margin-top:-2rem;position:absolute;top:50%}.ie9 .admin__data-grid-loading-mask .spinner{background:url(../images/loader-2.gif) 50% 50% no-repeat;bottom:0;height:149px;left:0;margin:auto;position:absolute;right:0;top:0;width:218px}.data-grid-cell-content{display:inline-block;overflow:hidden;width:100%}body._in-resize{cursor:col-resize;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body._in-resize *,body._in-resize .data-grid-th,body._in-resize .data-grid-th._draggable,body._in-resize .data-grid-th._sortable{cursor:col-resize!important}._layout-fixed{table-layout:fixed}.data-grid{border:none;font-size:1.3rem;margin-bottom:0;width:100%}.data-grid:not(._dragging-copy) ._odd-row td._dragging{background-color:#d0d0d0}.data-grid:not(._dragging-copy) ._dragging{background-color:#d9d9d9;color:rgba(48,48,48,.95)}.data-grid:not(._dragging-copy) ._dragging a{color:rgba(0,139,219,.95)}.data-grid:not(._dragging-copy) ._dragging a:hover{color:rgba(15,167,255,.95)}.data-grid._dragged{outline:#007bdb solid 1px}.data-grid thead{background-color:transparent}.data-grid tfoot th{padding:1rem}.data-grid tr._odd-row td{background-color:#f5f5f5}.data-grid tr._odd-row td._update-status-active{background:#89e1ff}.data-grid tr._odd-row td._update-status-upcoming{background:#b7ee63}.data-grid tr:hover td._update-status-active,.data-grid tr:hover td._update-status-upcoming{background-color:#e5f7fe}.data-grid tr.data-grid-tr-no-data td{font-size:1.6rem;padding:3rem;text-align:center}.data-grid tr.data-grid-tr-no-data:hover td{background-color:#fff;cursor:default}.data-grid tr:active td{background-color:#e0f6fe}.data-grid tr:hover td{background-color:#e5f7fe}.data-grid tr._dragged td{background:#d0d0d0}.data-grid tr._dragover-top td{box-shadow:inset 0 3px 0 0 #008bdb}.data-grid tr._dragover-bottom td{box-shadow:inset 0 -3px 0 0 #008bdb}.data-grid tr:not(.data-grid-editable-row):last-child td{border-bottom:.1rem solid #d6d6d6}.data-grid tr ._clickable,.data-grid tr._clickable{cursor:pointer}.data-grid tr._disabled{pointer-events:none}.data-grid td,.data-grid th{font-size:1.3rem;line-height:1.36;transition:background-color .1s linear;vertical-align:top}.data-grid td._resizing,.data-grid th._resizing{border-left:1px solid #007bdb;border-right:1px solid #007bdb}.data-grid td._hidden,.data-grid th._hidden{display:none}.data-grid td._fit,.data-grid th._fit{width:1%}.data-grid td{background-color:#fff;border-left:.1rem dashed #d6d6d6;border-right:.1rem dashed #d6d6d6;color:#303030;padding:1rem}.data-grid td:first-child{border-left-style:solid}.data-grid td:last-child{border-right-style:solid}.data-grid td .action-select-wrap{position:static}.data-grid td .action-select{color:#008bdb;text-decoration:none;background-color:transparent;border:none;font-size:1.3rem;padding:0 3rem 0 0;position:relative}.data-grid td .action-select:hover{color:#0fa7ff;text-decoration:underline}.data-grid td .action-select:hover:after{border-color:#0fa7ff transparent transparent}.data-grid td .action-select:after{border-color:#008bdb transparent transparent;margin:.6rem 0 0 .7rem;right:auto;top:auto}.data-grid td .action-select:before{display:none}.data-grid td .abs-action-menu .action-submenu,.data-grid td .abs-action-menu .action-submenu .action-submenu,.data-grid td .action-menu,.data-grid td .action-menu .action-submenu,.data-grid td .actions-split .action-menu .action-submenu,.data-grid td .actions-split .action-menu .action-submenu .action-submenu,.data-grid td .actions-split .dropdown-menu .action-submenu,.data-grid td .actions-split .dropdown-menu .action-submenu .action-submenu{left:auto;min-width:10rem;right:0;text-align:left;top:auto;z-index:1}.data-grid td._update-status-active{background:#bceeff}.data-grid td._update-status-upcoming{background:#ccf391}.data-grid th{background-color:#514943;border:.1rem solid #8a837f;border-left-color:transparent;color:#fff;font-weight:600;padding:0;text-align:left}.data-grid th:first-child{border-left-color:#8a837f}.data-grid th._dragover-left{box-shadow:inset 3px 0 0 0 #fff;z-index:2}.data-grid th._dragover-right{box-shadow:inset -3px 0 0 0 #fff}.data-grid .shadow-div{cursor:col-resize;height:100%;margin-right:-5px;position:absolute;right:0;top:0;width:10px}.data-grid .data-grid-th{background-clip:padding-box;color:#fff;padding:1rem;position:relative;vertical-align:middle}.data-grid .data-grid-th._resize-visible .shadow-div{cursor:auto;display:none}.data-grid .data-grid-th._draggable{cursor:grab}.data-grid .data-grid-th._sortable{cursor:pointer;transition:background-color .1s linear;z-index:1}.data-grid .data-grid-th._sortable:focus,.data-grid .data-grid-th._sortable:hover{background-color:#5f564f}.data-grid .data-grid-th._sortable:active{padding-bottom:.9rem;padding-top:1.1rem}.data-grid .data-grid-th.required>span:after{color:#f38a5e;content:'*';margin-left:.3rem}.data-grid .data-grid-checkbox-cell{overflow:hidden;padding:0;vertical-align:top;width:5.2rem}.data-grid .data-grid-checkbox-cell:hover{cursor:default}.data-grid .data-grid-thumbnail-cell{text-align:center;width:7rem}.data-grid .data-grid-thumbnail-cell img{border:1px solid #d6d6d6;width:5rem}.data-grid .data-grid-multicheck-cell{padding:1rem 1rem .9rem;text-align:center;vertical-align:middle}.data-grid .data-grid-onoff-cell{text-align:center;width:12rem}.data-grid .data-grid-actions-cell{padding-left:2rem;padding-right:2rem;text-align:center;width:1%}.data-grid._hidden{display:none}.data-grid._dragging-copy{box-shadow:1px 1px 5px rgba(0,0,0,.5);left:0;opacity:.95;position:fixed;top:0;z-index:1000}.data-grid._dragging-copy .data-grid-th{border:1px solid #007bdb;border-bottom:none}.data-grid._dragging-copy .data-grid-th,.data-grid._dragging-copy .data-grid-th._sortable{cursor:grabbing}.data-grid._dragging-copy tr:last-child td{border-bottom:1px solid #007bdb}.data-grid._dragging-copy td{border-left:1px solid #007bdb;border-right:1px solid #007bdb}.data-grid._dragging-copy._in-edit .data-grid-editable-row.data-grid-bulk-edit-panel td,.data-grid._dragging-copy._in-edit .data-grid-editable-row.data-grid-bulk-edit-panel td:before,.data-grid._dragging-copy._in-edit .data-grid-editable-row.data-grid-bulk-edit-panel:hover td{background-color:rgba(255,251,230,.95)}.data-grid._dragging-copy._in-edit .data-grid-editable-row td,.data-grid._dragging-copy._in-edit .data-grid-editable-row:hover td{background-color:rgba(255,255,255,.95)}.data-grid._dragging-copy._in-edit .data-grid-editable-row td:after,.data-grid._dragging-copy._in-edit .data-grid-editable-row td:before{left:0;right:0}.data-grid._dragging-copy._in-edit .data-grid-editable-row td:before{background-color:rgba(255,255,255,.95)}.data-grid._dragging-copy._in-edit .data-grid-editable-row td:only-child{border-left:1px solid #007bdb;border-right:1px solid #007bdb;left:0}.data-grid._dragging-copy._in-edit .data-grid-editable-row .admin__control-select,.data-grid._dragging-copy._in-edit .data-grid-editable-row .admin__control-text{opacity:.5}.data-grid .data-grid-controls-row td{padding-top:1.6rem}.data-grid .data-grid-controls-row td.data-grid-checkbox-cell{padding-top:.6rem}.data-grid .data-grid-controls-row td [class*=admin__control-],.data-grid .data-grid-controls-row td button{margin-top:-1.7rem}.data-grid._in-edit tr:hover td{background-color:#e6e6e6}.data-grid._in-edit ._odd-row.data-grid-editable-row td,.data-grid._in-edit ._odd-row.data-grid-editable-row:hover td{background-color:#fff}.data-grid._in-edit ._odd-row td,.data-grid._in-edit ._odd-row:hover td{background-color:#dcdcdc}.data-grid._in-edit .data-grid-editable-row-actions td,.data-grid._in-edit .data-grid-editable-row-actions:hover td{background-color:#fff}.data-grid._in-edit td{background-color:#e6e6e6;pointer-events:none}.data-grid._in-edit .data-grid-checkbox-cell{pointer-events:auto}.data-grid._in-edit .data-grid-editable-row{border:.1rem solid #adadad;border-bottom-color:#c2c2c2}.data-grid._in-edit .data-grid-editable-row:hover td{background-color:#fff}.data-grid._in-edit .data-grid-editable-row td{background-color:#fff;border-bottom-color:#fff;border-left-style:hidden;border-right-style:hidden;border-top-color:#fff;pointer-events:auto;vertical-align:middle}.data-grid._in-edit .data-grid-editable-row td:first-child{border-left-color:#adadad;border-left-style:solid}.data-grid._in-edit .data-grid-editable-row td:first-child:after,.data-grid._in-edit .data-grid-editable-row td:first-child:before{left:0}.data-grid._in-edit .data-grid-editable-row td:last-child{border-right-color:#adadad;border-right-style:solid;left:-.1rem}.data-grid._in-edit .data-grid-editable-row td:last-child:after,.data-grid._in-edit .data-grid-editable-row td:last-child:before{right:0}.data-grid._in-edit .data-grid-editable-row .admin__control-select,.data-grid._in-edit .data-grid-editable-row .admin__control-text{width:100%}.data-grid._in-edit .data-grid-bulk-edit-panel td{vertical-align:bottom}.data-grid .data-grid-editable-row td{border-left-color:#fff;border-left-style:solid;position:relative;z-index:1}.data-grid .data-grid-editable-row td:after{bottom:0;box-shadow:0 5px 5px rgba(0,0,0,.25);content:'';height:.9rem;left:0;margin-top:-1rem;position:absolute;right:0}.data-grid .data-grid-editable-row td:before{background-color:#fff;bottom:0;content:'';height:1rem;left:-10px;position:absolute;right:-10px;z-index:1}.data-grid .data-grid-editable-row.data-grid-editable-row-actions td,.data-grid .data-grid-editable-row.data-grid-editable-row-actions:hover td{background-color:#fff}.data-grid .data-grid-editable-row.data-grid-editable-row-actions td:first-child{border-left-color:#fff;border-right-color:#fff}.data-grid .data-grid-editable-row.data-grid-editable-row-actions td:last-child{left:0}.data-grid .data-grid-editable-row.data-grid-bulk-edit-panel td,.data-grid .data-grid-editable-row.data-grid-bulk-edit-panel td:before,.data-grid .data-grid-editable-row.data-grid-bulk-edit-panel:hover td{background-color:#fffbe6}.data-grid .data-grid-editable-row-actions{left:50%;margin-left:-12.5rem;margin-top:-2px;position:absolute;text-align:center}.data-grid .data-grid-editable-row-actions td{width:25rem}.data-grid .data-grid-editable-row-actions [class*=action-]{min-width:9rem}.data-grid .data-grid-draggable-row-cell{width:1%}.data-grid .data-grid-draggable-row-cell .draggable-handle{padding:0}.data-grid-th._sortable._ascend,.data-grid-th._sortable._descend{padding-right:2.7rem}.data-grid-th._sortable._ascend:before,.data-grid-th._sortable._descend:before{margin-top:-1em;position:absolute;right:1rem;top:50%}.data-grid-th._sortable._ascend:before{content:'\2193'}.data-grid-th._sortable._descend:before{content:'\2191'}.data-grid-checkbox-cell-inner{display:block;padding:1.1rem 1.8rem .9rem;text-align:right}.data-grid-checkbox-cell-inner:hover{cursor:pointer}.data-grid-state-cell-inner{display:block;padding:1.1rem 1.8rem .9rem;text-align:center}.data-grid-state-cell-inner>span{display:inline-block;font-style:italic;padding:.6rem 0}.data-grid-row-parent._active>td .data-grid-checkbox-cell-inner:before{content:'\e62b'}.data-grid-row-parent>td .data-grid-checkbox-cell-inner{padding-left:3.7rem;position:relative}.data-grid-row-parent>td .data-grid-checkbox-cell-inner:before{content:'\e628';font-size:1rem;font-weight:700;left:1.35rem;position:absolute;top:1.6rem}.data-grid-th._col-xs{width:1%}.data-grid-info-panel{box-shadow:0 0 5px rgba(0,0,0,.5);margin:2rem .1rem -2rem}.data-grid-info-panel .messages{overflow:hidden}.data-grid-info-panel .messages .message{margin:1rem}.data-grid-info-panel .messages .message:last-child{margin-bottom:1rem}.data-grid-info-panel-actions{padding:1rem;text-align:right}.data-grid-editable-row .admin__field-control{position:relative}.data-grid-editable-row .admin__field-control._error:after{border-color:transparent #ee7d7d transparent transparent;border-style:solid;border-width:0 12px 12px 0;content:'';position:absolute;right:0;top:0}.data-grid-editable-row .admin__field-control._error .admin__control-text{border-color:#ee7d7d}.data-grid-editable-row .admin__field-control._focus:after{display:none}.data-grid-editable-row .admin__field-error{bottom:100%;box-shadow:1px 1px 5px rgba(0,0,0,.5);left:0;margin:0 auto 1.5rem;max-width:32rem;position:absolute;right:0}.data-grid-editable-row .admin__field-error:after,.data-grid-editable-row .admin__field-error:before{border-style:solid;content:'';left:50%;position:absolute;top:100%}.data-grid-editable-row .admin__field-error:after{border-color:#fffbbb transparent transparent;border-width:10px 10px 0;margin-left:-10px;z-index:1}.data-grid-editable-row .admin__field-error:before{border-color:#ee7d7d transparent transparent;border-width:11px 12px 0;margin-left:-12px}.data-grid-bulk-edit-panel .admin__field-label-vertical{display:block;font-size:1.2rem;margin-bottom:.5rem;text-align:left}.data-grid-row-changed{cursor:default;display:block;opacity:.5;position:relative;width:100%;z-index:1}.data-grid-row-changed:after{content:'\e631';display:inline-block}.data-grid-row-changed .data-grid-row-changed-tooltip{background:#f1f1f1;border:1px solid #f1f1f1;border-radius:1px;bottom:100%;box-shadow:0 3px 9px 0 rgba(0,0,0,.3);display:none;font-weight:400;line-height:1.36;margin-bottom:1.5rem;padding:1rem;position:absolute;right:-1rem;text-transform:none;width:27rem;word-break:normal;z-index:2}.data-grid-row-changed._changed{opacity:1;z-index:3}.data-grid-row-changed._changed:hover .data-grid-row-changed-tooltip{display:block}.data-grid-row-changed._changed:hover:before{background:#f1f1f1;border:1px solid #f1f1f1;bottom:100%;box-shadow:4px 4px 3px -1px rgba(0,0,0,.15);content:'';display:block;height:1.6rem;left:50%;margin:0 0 .7rem -.8rem;position:absolute;-ms-transform:rotate(45deg);transform:rotate(45deg);width:1.6rem;z-index:3}.ie9 .data-grid-row-changed._changed:hover:before{display:none}.admin__data-grid-outer-wrap .data-grid-checkbox-cell{overflow:hidden}.admin__data-grid-outer-wrap .data-grid-checkbox-cell-inner{position:relative}.admin__data-grid-outer-wrap .data-grid-checkbox-cell-inner:before{bottom:0;content:'';height:500%;left:0;position:absolute;right:0;top:0}.admin__data-grid-wrap-static .data-grid-checkbox-cell:hover{cursor:pointer}.admin__data-grid-wrap-static .data-grid-checkbox-cell-inner{margin:1.1rem 1.8rem .9rem;padding:0}.adminhtml-cms-hierarchy-index .admin__data-grid-wrap-static .data-grid-actions-cell:first-child{padding:0}.adminhtml-export-index .admin__data-grid-wrap-static .data-grid-checkbox-cell-inner{margin:0;padding:1.1rem 1.8rem 1.9rem}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child:before,.admin__control-file-label:before,.admin__control-multiselect,.admin__control-select,.admin__control-text,.admin__control-textarea,.selectmenu{-webkit-appearance:none;background-color:#fff;border:1px solid #adadad;border-radius:1px;box-shadow:none;color:#303030;font-size:1.4rem;font-weight:400;height:auto;line-height:1.36;padding:.6rem 1rem;transition:border-color .1s linear;vertical-align:baseline;width:auto}.admin__control-addon [class*=admin__control-][class]:hover~[class*=admin__addon-]:last-child:before,.admin__control-multiselect:hover,.admin__control-select:hover,.admin__control-text:hover,.admin__control-textarea:hover,.selectmenu:hover,.selectmenu:hover .selectmenu-toggle:before{border-color:#878787}.admin__control-addon [class*=admin__control-][class]:focus~[class*=admin__addon-]:last-child:before,.admin__control-file:active+.admin__control-file-label:before,.admin__control-file:focus+.admin__control-file-label:before,.admin__control-multiselect:focus,.admin__control-select:focus,.admin__control-text:focus,.admin__control-textarea:focus,.selectmenu._focus,.selectmenu._focus .selectmenu-toggle:before{border-color:#007bdb;box-shadow:none;outline:0}.admin__control-addon [class*=admin__control-][class][disabled]~[class*=admin__addon-]:last-child:before,.admin__control-file[disabled]+.admin__control-file-label:before,.admin__control-multiselect[disabled],.admin__control-select[disabled],.admin__control-text[disabled],.admin__control-textarea[disabled]{background-color:#e9e9e9;border-color:#adadad;color:#303030;cursor:not-allowed;opacity:.5}.admin__field-row[class]>.admin__field-control,.admin__fieldset>.admin__field.admin__field-wide[class]>.admin__field-control{clear:left;float:none;text-align:left;width:auto}.admin__field-row[class]:not(.admin__field-option)>.admin__field-label,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)>.admin__field-label{display:block;line-height:1.4rem;margin-bottom:.86rem;margin-top:-.14rem;text-align:left;width:auto}.admin__field-row[class]:not(.admin__field-option)>.admin__field-label:before,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)>.admin__field-label:before{display:none}.admin__field-row[class]:not(.admin__field-option)._required>.admin__field-label span,.admin__field-row[class]:not(.admin__field-option).required>.admin__field-label span,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)._required>.admin__field-label span,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option).required>.admin__field-label span{padding-left:1.5rem}.admin__field-row[class]:not(.admin__field-option)._required>.admin__field-label span:after,.admin__field-row[class]:not(.admin__field-option).required>.admin__field-label span:after,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)._required>.admin__field-label span:after,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option).required>.admin__field-label span:after{left:0;margin-left:30px}.admin__legend{font-size:1.8rem;font-weight:600;margin-bottom:3rem}.admin__control-checkbox,.admin__control-radio{cursor:pointer;opacity:.01;overflow:hidden;position:absolute;vertical-align:top}.admin__control-checkbox:after,.admin__control-radio:after{display:none}.admin__control-checkbox+label,.admin__control-radio+label{cursor:pointer;display:inline-block}.admin__control-checkbox+label:before,.admin__control-radio+label:before{background-color:#fff;border:1px solid #adadad;color:transparent;float:left;height:1.6rem;text-align:center;vertical-align:top;width:1.6rem}.admin__control-checkbox+.admin__field-label,.admin__control-radio+.admin__field-label{padding-left:2.6rem}.admin__control-checkbox+.admin__field-label:before,.admin__control-radio+.admin__field-label:before{margin:1px 1rem 0 -2.6rem}.admin__control-checkbox:checked+label:before,.admin__control-radio:checked+label:before{color:#514943}.admin__control-checkbox.disabled+label,.admin__control-checkbox[disabled]+label,.admin__control-radio.disabled+label,.admin__control-radio[disabled]+label{color:#303030;cursor:default;opacity:.5}.admin__control-checkbox.disabled+label:before,.admin__control-checkbox[disabled]+label:before,.admin__control-radio.disabled+label:before,.admin__control-radio[disabled]+label:before{background-color:#e9e9e9;border-color:#adadad;cursor:default}._keyfocus .admin__control-checkbox:not(.disabled):focus+label:before,._keyfocus .admin__control-checkbox:not([disabled]):focus+label:before,._keyfocus .admin__control-radio:not(.disabled):focus+label:before,._keyfocus .admin__control-radio:not([disabled]):focus+label:before{border-color:#007bdb}.admin__control-checkbox:not(.disabled):hover+label:before,.admin__control-checkbox:not([disabled]):hover+label:before,.admin__control-radio:not(.disabled):hover+label:before,.admin__control-radio:not([disabled]):hover+label:before{border-color:#878787}.admin__control-radio+label:before{border-radius:1.6rem;content:'';transition:border-color .1s linear,color .1s ease-in}.admin__control-radio.admin__control-radio+label:before{line-height:140%}.admin__control-radio:checked+label{position:relative}.admin__control-radio:checked+label:after{background-color:#514943;border-radius:50%;content:'';height:10px;left:3px;position:absolute;top:4px;width:10px}.admin__control-radio:checked:not(.disabled):hover,.admin__control-radio:checked:not(.disabled):hover+label,.admin__control-radio:checked:not([disabled]):hover,.admin__control-radio:checked:not([disabled]):hover+label{cursor:default}.admin__control-radio:checked:not(.disabled):hover+label:before,.admin__control-radio:checked:not([disabled]):hover+label:before{border-color:#adadad}.admin__control-checkbox+label:before{border-radius:1px;content:'';font-size:0;transition:font-size .1s ease-out,color .1s ease-out,border-color .1s linear}.admin__control-checkbox:checked+label:before{content:'\e62d';font-size:1.1rem;line-height:125%}.admin__control-checkbox:not(:checked)._indeterminate+label:before,.admin__control-checkbox:not(:checked):indeterminate+label:before{color:#514943;content:'-';font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:700}input[type=checkbox].admin__control-checkbox,input[type=radio].admin__control-checkbox{margin:0;position:absolute}.admin__control-text{min-width:4rem}.admin__control-select{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;background-image:url(../images/arrows-bg.svg),linear-gradient(#e3e3e3,#e3e3e3),linear-gradient(#adadad,#adadad);background-position:calc(100% - 12px) -34px,100%,calc(100% - 3.2rem) 0;background-size:auto,3.2rem 100%,1px 100%;background-repeat:no-repeat;max-width:100%;min-width:8.5rem;padding-bottom:.5rem;padding-right:4.4rem;padding-top:.5rem;transition:border-color .1s linear}.admin__control-select:hover{border-color:#878787;cursor:pointer}.admin__control-select:focus{background-image:url(../images/arrows-bg.svg),linear-gradient(#e3e3e3,#e3e3e3),linear-gradient(#007bdb,#007bdb);background-position:calc(100% - 12px) 13px,100%,calc(100% - 3.2rem) 0;border-color:#007bdb}.admin__control-select::-ms-expand{display:none}.ie9 .admin__control-select{background-image:none;padding-right:1rem}option:empty{display:none}.admin__control-multiselect{height:auto;max-width:100%;min-width:15rem;overflow:auto;padding:0;resize:both}.admin__control-multiselect optgroup,.admin__control-multiselect option{padding:.5rem 1rem}.admin__control-file-wrapper{display:inline-block;padding:.5rem 1rem;position:relative;z-index:1}.admin__control-file-label:before{content:'';left:0;position:absolute;top:0;width:100%;z-index:0}.admin__control-file{background:0 0;border:0;padding-top:.7rem;position:relative;width:auto;z-index:1}.admin__control-support-text{border:1px solid transparent;display:inline-block;font-size:1.4rem;line-height:1.36;padding-bottom:.6rem;padding-top:.6rem}.admin__control-support-text+[class*=admin__control-],[class*=admin__control-]+.admin__control-support-text{margin-left:.7rem}.admin__control-service{float:left;margin:.8rem 0 0 3rem}.admin__control-textarea{height:8.48rem;line-height:1.18;padding-top:.8rem;resize:vertical}.admin__control-addon{-ms-flex-direction:row;flex-direction:row;display:inline-flex;-ms-flex-flow:row nowrap;flex-flow:row nowrap;position:relative;width:100%;z-index:1}.admin__control-addon>[class*=admin__addon-],.admin__control-addon>[class*=admin__control-]{-ms-flex-preferred-size:auto;flex-basis:auto;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0;position:relative;z-index:1}.admin__control-addon .admin__control-select{width:auto}.admin__control-addon .admin__control-text{margin:.1rem;padding:.5rem .9rem;width:100%}.admin__control-addon [class*=admin__control-][class]{appearence:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-order:1;order:1;-ms-flex-negative:1;flex-shrink:1;background-color:transparent;border-color:transparent;box-shadow:none;vertical-align:top}.admin__control-addon [class*=admin__control-][class]+[class*=admin__control-]{border-left-color:#adadad}.admin__control-addon [class*=admin__control-][class] :focus{box-shadow:0}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child{padding-left:1rem;position:static!important;z-index:0}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child>*{position:relative;vertical-align:top;z-index:1}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child:empty{padding:0}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child:before{bottom:0;box-sizing:border-box;content:'';left:0;position:absolute;top:0;width:100%;z-index:-1}.admin__addon-prefix,.admin__addon-suffix{border:0;box-sizing:border-box;color:#858585;display:inline-block;font-size:1.4rem;font-weight:400;height:3.2rem;line-height:3.2rem;padding:0}.admin__addon-suffix{-ms-flex-order:3;order:3}.admin__addon-suffix:last-child{padding-right:1rem}.admin__addon-prefix{-ms-flex-order:0;order:0}.ie9 .admin__control-addon:after{clear:both;content:'';display:block;height:0;overflow:hidden}.ie9 .admin__addon{min-width:0;overflow:hidden;text-align:right;white-space:nowrap;width:auto}.ie9 .admin__addon [class*=admin__control-]{display:inline}.ie9 .admin__addon-prefix{float:left}.ie9 .admin__addon-suffix{float:right}.admin__control-collapsible{width:100%}.admin__control-collapsible ._dragged .admin__collapsible-block-wrapper .admin__collapsible-title{background:#d0d0d0}.admin__control-collapsible ._dragover-bottom .admin__collapsible-block-wrapper:before,.admin__control-collapsible ._dragover-top .admin__collapsible-block-wrapper:before{background:#008bdb;content:'';display:block;height:3px;left:0;position:absolute;right:0}.admin__control-collapsible ._dragover-top .admin__collapsible-block-wrapper:before{top:-3px}.admin__control-collapsible ._dragover-bottom .admin__collapsible-block-wrapper:before{bottom:-3px}.admin__control-collapsible .admin__collapsible-block-wrapper.fieldset-wrapper{border:0;margin:0;position:relative}.admin__control-collapsible .admin__collapsible-block-wrapper.fieldset-wrapper .fieldset-wrapper-title{background:#f8f8f8;border:2px solid #ccc}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .admin__collapsible-title{font-size:1.4rem;font-weight:400;line-height:1;padding:1.6rem 4rem 1.6rem 3.8rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .admin__collapsible-title:before{left:1rem;right:auto;top:1.4rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete{background-color:transparent;border-color:transparent;box-shadow:none;padding:0;position:absolute;right:1rem;top:1.4rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:hover{background-color:transparent;border-color:transparent;box-shadow:none}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:before{content:'\e630';font-size:2rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete>span{display:none}.admin__control-collapsible .admin__collapsible-content{background-color:#fff;margin-bottom:1rem}.admin__control-collapsible .admin__collapsible-content>.fieldset-wrapper{border:1px solid #ccc;margin-top:-1px;padding:1rem}.admin__control-collapsible .admin__collapsible-content .admin__fieldset{padding:0}.admin__control-collapsible .admin__collapsible-content .admin__field:last-child{margin-bottom:0}.admin__control-table-wrapper{max-width:100%;overflow-x:auto;overflow-y:hidden}.admin__control-table{width:100%}.admin__control-table thead{background-color:transparent}.admin__control-table tbody td{vertical-align:top}.admin__control-table tfoot th{padding-bottom:1.3rem}.admin__control-table tfoot th.validation{padding-bottom:0;padding-top:0}.admin__control-table tfoot td{border-top:1px solid #fff}.admin__control-table tfoot .admin__control-table-pagination{float:right;padding-bottom:0}.admin__control-table tfoot .action-previous{margin-right:.5rem}.admin__control-table tfoot .action-next{margin-left:.9rem}.admin__control-table tr:last-child td{border-bottom:none}.admin__control-table tr._dragover-top td{box-shadow:inset 0 3px 0 0 #008bdb}.admin__control-table tr._dragover-bottom td{box-shadow:inset 0 -3px 0 0 #008bdb}.admin__control-table tr._dragged td,.admin__control-table tr._dragged th{background:#d0d0d0}.admin__control-table td,.admin__control-table th{background-color:#efefef;border:0;border-bottom:1px solid #fff;padding:1.3rem 1rem 1.3rem 0;text-align:left;vertical-align:top}.admin__control-table td:first-child,.admin__control-table th:first-child{padding-left:1rem}.admin__control-table td>.admin__control-select,.admin__control-table td>.admin__control-text,.admin__control-table th>.admin__control-select,.admin__control-table th>.admin__control-text{width:100%}.admin__control-table td._hidden,.admin__control-table th._hidden{display:none}.admin__control-table td._fit,.admin__control-table th._fit{width:1px}.admin__control-table th{color:#303030;font-size:1.4rem;font-weight:600;vertical-align:bottom}.admin__control-table th._required span:after{color:#eb5202;content:'*'}.admin__control-table .control-table-actions-th{white-space:nowrap}.admin__control-table .control-table-actions-cell{padding-top:1.8rem;text-align:center;width:1%}.admin__control-table .control-table-options-th{text-align:center;width:10rem}.admin__control-table .control-table-options-cell{text-align:center}.admin__control-table .control-table-text{line-height:3.2rem}.admin__control-table .col-draggable{padding-top:2.2rem;width:1%}.admin__control-table .action-delete{background-color:transparent;border-color:transparent;box-shadow:none;padding-left:0;padding-right:0}.admin__control-table .action-delete:hover{background-color:transparent;border-color:transparent;box-shadow:none}.admin__control-table .action-delete:before{content:'\e630';font-size:2rem}.admin__control-table .action-delete>span{display:none}.admin__control-table .draggable-handle{padding:0}.admin__control-table._dragged{outline:#007bdb solid 1px}.admin__control-table-action{background-color:#efefef;border-top:1px solid #fff;padding:1.3rem 1rem}.admin__dynamic-rows._dragged{opacity:.95;position:absolute;z-index:999}.admin__dynamic-rows.admin__control-table .admin__control-fields>.admin__field{border:0;padding:0}.admin__dynamic-rows td>.admin__field{border:0;margin:0;padding:0}.admin__control-table-pagination{padding-bottom:1rem}.admin__control-table-pagination .admin__data-grid-pager{float:right}.admin__field-tooltip{display:inline-block;margin-top:.5rem;max-width:45px;overflow:visible;vertical-align:top;width:0}.admin__field-tooltip:hover{position:relative;z-index:500}.admin__field-option .admin__field-tooltip{margin-top:.5rem}.admin__field-tooltip .admin__field-tooltip-action{margin-left:2rem;position:relative;z-index:2;display:inline-block;text-decoration:none}.admin__field-tooltip .admin__field-tooltip-action:before{-webkit-font-smoothing:antialiased;font-size:2.2rem;line-height:1;color:#514943;content:'\e633';font-family:Icons;vertical-align:middle;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.admin__field-tooltip .admin__control-text:focus+.admin__field-tooltip-content,.admin__field-tooltip:hover .admin__field-tooltip-content{display:block}.admin__field-tooltip .admin__field-tooltip-content{bottom:3.8rem;display:none;right:-2.3rem}.admin__field-tooltip .admin__field-tooltip-content:after,.admin__field-tooltip .admin__field-tooltip-content:before{border:1.6rem solid transparent;height:0;width:0;border-top-color:#afadac;content:'';display:block;position:absolute;right:2rem;top:100%;z-index:3}.admin__field-tooltip .admin__field-tooltip-content:after{border-top-color:#fffbbb;margin-top:-1px;z-index:4}.abs-admin__field-tooltip-content,.admin__field-tooltip .admin__field-tooltip-content{box-shadow:0 2px 8px 0 rgba(0,0,0,.3);background:#fffbbb;border:1px solid #afadac;border-radius:1px;padding:1.5rem 2.5rem;position:absolute;width:32rem;z-index:1}.admin__field-fallback-reset{font-size:1.25rem;white-space:nowrap;width:30px}.admin__field-fallback-reset>span{margin-left:.5rem;position:relative}.admin__field-fallback-reset:active{-ms-transform:scale(0.98);transform:scale(0.98)}.admin__field-fallback-reset:before{transition:color .1s linear;content:'\e642';font-size:1.3rem;margin-left:.5rem}.admin__field-fallback-reset:hover{cursor:pointer;text-decoration:none}.admin__field-fallback-reset:focus{background:0 0}.abs-field-size-x-small,.abs-field-sizes.admin__field-x-small>.admin__field-control,.admin__field.admin__field-x-small>.admin__field-control,.admin__fieldset>.admin__field.admin__field-x-small>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-x-small>.admin__field-control{width:8rem}.abs-field-size-small,.abs-field-sizes.admin__field-small>.admin__field-control,.admin__control-grouped-date>.admin__field-date.admin__field>.admin__field-control,.admin__field.admin__field-small>.admin__field-control,.admin__fieldset>.admin__field.admin__field-small>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-small>.admin__field-control{width:15rem}.abs-field-size-medium,.abs-field-sizes.admin__field-medium>.admin__field-control,.admin__field.admin__field-medium>.admin__field-control,.admin__fieldset>.admin__field.admin__field-medium>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-medium>.admin__field-control{width:34rem}.abs-field-size-large,.abs-field-sizes.admin__field-large>.admin__field-control,.admin__field.admin__field-large>.admin__field-control,.admin__fieldset>.admin__field.admin__field-large>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-large>.admin__field-control{width:64rem}.abs-field-no-label,.admin__field-group-additional,.admin__field-no-label,.admin__fieldset>.admin__field.admin__field-no-label>.admin__field-control{margin-left:calc((100%) * .25 + 30px)}.admin__fieldset{border:0;margin:0;min-width:0;padding:0}.admin__fieldset .fieldset-wrapper.admin__fieldset-section>.fieldset-wrapper-title{padding-left:1rem}.admin__fieldset .fieldset-wrapper.admin__fieldset-section>.fieldset-wrapper-title strong{font-size:1.7rem;font-weight:600}.admin__fieldset .fieldset-wrapper.admin__fieldset-section .admin__fieldset-wrapper-content>.admin__fieldset{padding-top:1rem}.admin__fieldset .fieldset-wrapper.admin__fieldset-section:last-child .admin__fieldset-wrapper-content>.admin__fieldset{padding-bottom:0}.admin__fieldset>.admin__field{border:0;margin:0 0 0 -30px;padding:0}.admin__fieldset>.admin__field:after{clear:both;content:'';display:table}.admin__fieldset>.admin__field>.admin__field-control{width:calc((100%) * .5 - 30px);float:left;margin-left:30px}.admin__fieldset>.admin__field>.admin__field-label{width:calc((100%) * .25 - 30px);float:left;margin-left:30px}.admin__fieldset>.admin__field.admin__field-no-label>.admin__field-label{display:none}.admin__fieldset>.admin__field+.admin__field._empty._no-header{margin-top:-3rem}.admin__fieldset-product-websites{position:relative;z-index:300}.admin__fieldset-note{margin-bottom:2rem}.admin__form-field{border:0;margin:0;padding:0}.admin__field-control .admin__control-text,.admin__field-control .admin__control-textarea,.admin__form-field-control .admin__control-text,.admin__form-field-control .admin__control-textarea{width:100%}.admin__field-label{color:#303030;cursor:pointer;margin:0;text-align:right}.admin__field-label+br{display:none}.admin__field:not(.admin__field-option)>.admin__field-label{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:600;line-height:3.2rem;padding:0;white-space:nowrap}.admin__field:not(.admin__field-option)>.admin__field-label:before{opacity:0;visibility:hidden;content:'.';margin-left:-7px;overflow:hidden}.admin__field:not(.admin__field-option)>.admin__field-label span{display:inline-block;line-height:1.2;vertical-align:middle;white-space:normal}.admin__field:not(.admin__field-option)>.admin__field-label span[data-config-scope]{position:relative}._required>.admin__field-label>span:after,.required>.admin__field-label>span:after{color:#eb5202;content:'*';display:inline-block;font-size:1.6rem;font-weight:500;line-height:1;margin-left:10px;margin-top:.2rem;position:absolute;z-index:1}._disabled>.admin__field-label{color:#999;cursor:default}.admin__field{margin-bottom:0}.admin__field+.admin__field{margin-top:1.5rem}.admin__field:not(.admin__field-option)~.admin__field-option{margin-top:.5rem}.admin__field.admin__field-option~.admin__field-option{margin-top:.9rem}.admin__field~.admin__field-option:last-child{margin-bottom:.8rem}.admin__fieldset>.admin__field{margin-bottom:3rem;position:relative}.admin__field legend.admin__field-label{opacity:0}.admin__field[data-config-scope]:before{color:gray;content:attr(data-config-scope);display:inline-block;font-size:1.2rem;left:calc((100%) * .75 - 30px);line-height:3.2rem;margin-left:60px;position:absolute;width:calc((100%) * .25 - 30px)}.admin__field-control .admin__field[data-config-scope]:nth-child(n+2):before{content:''}.admin__field._error .admin__field-control [class*=admin__addon-]:before,.admin__field._error .admin__field-control [class*=admin__control-] [class*=admin__addon-]:before,.admin__field._error .admin__field-control>[class*=admin__control-]{border-color:#e22626}.admin__field._disabled,.admin__field._disabled:hover{box-shadow:inherit;cursor:inherit;opacity:1;outline:inherit}.admin__field._hidden{display:none}.admin__field-control+.admin__field-control{margin-top:1.5rem}.admin__field-control._with-tooltip>.admin__control-addon,.admin__field-control._with-tooltip>.admin__control-select,.admin__field-control._with-tooltip>.admin__control-text,.admin__field-control._with-tooltip>.admin__control-textarea,.admin__field-control._with-tooltip>.admin__field-option{max-width:calc(100% - 45px - 4px)}.admin__field-control._with-tooltip .admin__field-tooltip{width:auto}.admin__field-control._with-tooltip .admin__field-option{display:inline-block}.admin__field-control._with-reset>.admin__control-addon,.admin__field-control._with-reset>.admin__control-text,.admin__field-control._with-reset>.admin__control-textarea{width:calc(100% - 30px - .5rem - 4px)}.admin__field-control._with-reset .admin__field-fallback-reset{margin-left:.5rem;margin-top:1rem;vertical-align:top}.admin__field-control._with-reset._with-tooltip>.admin__control-addon,.admin__field-control._with-reset._with-tooltip>.admin__control-text,.admin__field-control._with-reset._with-tooltip>.admin__control-textarea{width:calc(100% - 30px - .5rem - 45px - 8px)}.admin__fieldset>.admin__field-collapsible{margin-bottom:0}.admin__fieldset>.admin__field-collapsible .admin__field-control{border-top:1px solid #ccc;display:block;font-size:1.7rem;font-weight:700;padding:1.7rem 0;width:calc(97%)}.admin__fieldset>.admin__field-collapsible .admin__field-option{padding-top:0}.admin__field-collapsible+div{margin-top:2.5rem}.admin__field-collapsible .admin__control-radio+label:before{height:1.8rem;width:1.8rem}.admin__field-collapsible .admin__control-radio:checked+label:after{left:4px;top:5px}.admin__field-error{background:#fffbbb;border:1px solid #ee7d7d;box-sizing:border-box;color:#555;display:block;font-size:1.2rem;font-weight:400;line-height:1.2;margin:.2rem 0 0;padding:.8rem 1rem .9rem}.admin__field-note{color:#303030;font-size:1.2rem;margin:10px 0 0;padding:0}.admin__additional-info{padding-top:1rem}.admin__field-option{padding-top:.7rem}.admin__field-option .admin__field-label{text-align:left}.admin__field-control>.admin__field-option:nth-child(1):nth-last-child(2),.admin__field-control>.admin__field-option:nth-child(2):nth-last-child(1){display:inline-block}.admin__field-control>.admin__field-option:nth-child(1):nth-last-child(2)+.admin__field-option,.admin__field-control>.admin__field-option:nth-child(2):nth-last-child(1)+.admin__field-option{display:inline-block;margin-left:41px;margin-top:0}.admin__field-control>.admin__field-option:nth-child(1):nth-last-child(2)+.admin__field-option:before,.admin__field-control>.admin__field-option:nth-child(2):nth-last-child(1)+.admin__field-option:before{background:#cacaca;content:'';display:inline-block;height:20px;margin-left:-20px;position:absolute;width:1px}.admin__field-value{display:inline-block;padding-top:.7rem}.admin__field-service{padding-top:1rem}.admin__control-fields>.admin__field:first-child,[class*=admin__control-grouped]>.admin__field:first-child{position:static}.admin__control-fields>.admin__field:first-child>.admin__field-label,[class*=admin__control-grouped]>.admin__field:first-child>.admin__field-label{width:calc((100%) * .25 - 30px);float:left;margin-left:30px;background:#fff;cursor:pointer;left:0;position:absolute;top:0}.admin__control-fields>.admin__field:first-child>.admin__field-label span:before,[class*=admin__control-grouped]>.admin__field:first-child>.admin__field-label span:before{display:block}.admin__control-fields>.admin__field._disabled>.admin__field-label,[class*=admin__control-grouped]>.admin__field._disabled>.admin__field-label{cursor:default}.admin__control-fields>.admin__field>.admin__field-label span:before,[class*=admin__control-grouped]>.admin__field>.admin__field-label span:before{display:none}.admin__control-fields .admin__field-label~.admin__field-control{width:100%}.admin__control-fields .admin__field-option{padding-top:0}[class*=admin__control-grouped]{box-sizing:border-box;display:table;width:100%}[class*=admin__control-grouped]>.admin__field{display:table-cell;vertical-align:top}[class*=admin__control-grouped]>.admin__field>.admin__field-control{float:none;width:100%}[class*=admin__control-grouped]>.admin__field.admin__field-default,[class*=admin__control-grouped]>.admin__field.admin__field-large,[class*=admin__control-grouped]>.admin__field.admin__field-medium,[class*=admin__control-grouped]>.admin__field.admin__field-small,[class*=admin__control-grouped]>.admin__field.admin__field-x-small{width:1px}[class*=admin__control-grouped]>.admin__field.admin__field-default+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-large+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-medium+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-small+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-x-small+.admin__field:last-child{width:auto}[class*=admin__control-grouped]>.admin__field:nth-child(n+2){padding-left:20px}.admin__control-group-equal{table-layout:fixed}.admin__control-group-equal>.admin__field{width:50%}.admin__field-control-group{margin-top:.8rem}.admin__field-control-group>.admin__field{padding:0}.admin__control-grouped-date>.admin__field-date{white-space:nowrap;width:1px}.admin__control-grouped-date>.admin__field-date.admin__field>.admin__field-control{float:left;position:relative}.admin__control-grouped-date>.admin__field-date+.admin__field:last-child{width:auto}.admin__control-grouped-date>.admin__field-date+.admin__field-date>.admin__field-label{float:left;padding-right:20px}.admin__control-grouped-date .ui-datepicker-trigger{left:100%;top:0}.admin__field-group-columns.admin__field-control.admin__control-grouped{width:calc((100%) * 1 - 30px);float:left;margin-left:30px}.admin__field-group-columns>.admin__field:first-child>.admin__field-label{float:none;margin:0;opacity:1;position:static;text-align:left}.admin__field-group-columns .admin__control-select{width:100%}.admin__field-group-additional{clear:both}.admin__field-group-additional .action-advanced{margin-top:1rem}.admin__field-group-additional .action-secondary{width:100%}.admin__field-group-show-label{white-space:nowrap}.admin__field-group-show-label>.admin__field-control,.admin__field-group-show-label>.admin__field-label{display:inline-block;vertical-align:top}.admin__field-group-show-label>.admin__field-label{margin-right:20px}.admin__field-complex{margin:1rem 0 3rem;padding-left:1rem}.admin__field:not(._hidden)+.admin__field-complex{margin-top:3rem}.admin__field-complex .admin__field-complex-title{clear:both;color:#303030;font-size:1.7rem;font-weight:600;letter-spacing:.025em;margin-bottom:1rem}.admin__field-complex .admin__field-complex-elements{float:right;max-width:40%}.admin__field-complex .admin__field-complex-elements button{margin-left:1rem}.admin__field-complex .admin__field-complex-content{max-width:60%;overflow:hidden}.admin__field-complex .admin__field-complex-text{margin-left:-1rem}.admin__field-complex+.admin__field._empty._no-header{margin-top:-3rem}.admin__legend{float:left;position:static;width:100%}.admin__legend+br{clear:left;display:block;height:0;overflow:hidden}.message{margin-bottom:3rem}.message-icon-top:before{margin-top:0;top:1.8rem}.nav{background-color:#f8f8f8;border-bottom:1px solid #e3e3e3;border-top:1px solid #e3e3e3;display:none;margin-bottom:3rem;padding:2.2rem 1.5rem 0 0}.nav .btn-group,.nav-bar-outer-actions{float:right;margin-bottom:1.7rem}.nav .btn-group .btn-wrap,.nav-bar-outer-actions .btn-wrap{float:right;margin-left:.5rem;margin-right:.5rem}.nav .btn-group .btn-wrap .btn,.nav-bar-outer-actions .btn-wrap .btn{padding-left:.5rem;padding-right:.5rem}.nav-bar-outer-actions{margin-top:-10.6rem;padding-right:1.5rem}.btn-wrap-try-again{width:9.5rem}.btn-wrap-next,.btn-wrap-prev{width:8.5rem}.nav-bar{counter-reset:i;float:left;margin:0 1rem 1.7rem 0;padding:0;position:relative;white-space:nowrap}.nav-bar:before{background-color:#d4d4d4;background-repeat:repeat-x;background-image:linear-gradient(to bottom,#d1d1d1 0,#d4d4d4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#d1d1d1', endColorstr='#d4d4d4', GradientType=0);border-bottom:1px solid #d9d9d9;border-top:1px solid #bfbfbf;content:'';height:1rem;left:5.15rem;position:absolute;right:5.15rem;top:.7rem}.nav-bar>li{display:inline-block;font-size:0;position:relative;vertical-align:top;width:10.3rem}.nav-bar>li:first-child:after{display:none}.nav-bar>li:after{background-color:#514943;content:'';height:.5rem;left:calc(-50% + .25rem);position:absolute;right:calc(50% + .7rem);top:.9rem}.nav-bar>li.disabled:before,.nav-bar>li.ui-state-disabled:before{bottom:0;content:'';left:0;position:absolute;right:0;top:0;z-index:1}.nav-bar>li.active~li:after,.nav-bar>li.ui-state-active~li:after{display:none}.nav-bar>li.active~li a:after,.nav-bar>li.ui-state-active~li a:after{background-color:transparent;border-color:transparent;color:#a6a6a6}.nav-bar>li.active a,.nav-bar>li.ui-state-active a{color:#000}.nav-bar>li.active a:hover,.nav-bar>li.ui-state-active a:hover{cursor:default}.nav-bar>li.active a:after,.nav-bar>li.ui-state-active a:after{background-color:#fff;content:''}.nav-bar a{color:#514943;display:block;font-size:1.2rem;font-weight:600;line-height:1.2;overflow:hidden;padding:3rem .5em 0;position:relative;text-align:center;text-overflow:ellipsis}.nav-bar a:hover{text-decoration:none}.nav-bar a:after{background-color:#514943;border:.4rem solid #514943;border-radius:100%;color:#fff;content:counter(i);counter-increment:i;height:1.5rem;left:50%;line-height:.6;margin-left:-.8rem;position:absolute;right:auto;text-align:center;top:.4rem;width:1.5rem}.nav-bar a:before{background-color:#d6d6d6;border:1px solid transparent;border-bottom-color:#d9d9d9;border-radius:100%;border-top-color:#bfbfbf;content:'';height:2.3rem;left:50%;line-height:1;margin-left:-1.2rem;position:absolute;top:0;width:2.3rem}.tooltip{display:block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.19rem;font-weight:400;line-height:1.4;opacity:0;position:absolute;visibility:visible;z-index:10}.tooltip.in{opacity:.9}.tooltip.top{margin-top:-4px;padding:8px 0}.tooltip.right{margin-left:4px;padding:0 8px}.tooltip.bottom{margin-top:4px;padding:8px 0}.tooltip.left{margin-left:-4px;padding:0 8px}.tooltip p:last-child{margin-bottom:0}.tooltip-inner{background-color:#fff;border:1px solid #adadad;border-radius:0;box-shadow:1px 1px 1px #ccc;color:#41362f;max-width:31rem;padding:.5em 1em;text-decoration:none}.tooltip-arrow,.tooltip-arrow:after{border:solid transparent;height:0;position:absolute;width:0}.tooltip-arrow:after{content:'';position:absolute}.tooltip.top .tooltip-arrow,.tooltip.top .tooltip-arrow:after{border-top-color:#949494;border-width:8px 8px 0;bottom:0;left:50%;margin-left:-8px}.tooltip.top-left .tooltip-arrow,.tooltip.top-left .tooltip-arrow:after{border-top-color:#949494;border-width:8px 8px 0;bottom:0;margin-bottom:-8px;right:8px}.tooltip.top-right .tooltip-arrow,.tooltip.top-right .tooltip-arrow:after{border-top-color:#949494;border-width:8px 8px 0;bottom:0;left:8px;margin-bottom:-8px}.tooltip.right .tooltip-arrow,.tooltip.right .tooltip-arrow:after{border-right-color:#949494;border-width:8px 8px 8px 0;left:1px;margin-top:-8px;top:50%}.tooltip.right .tooltip-arrow:after{border-right-color:#fff;border-width:6px 7px 6px 0;margin-left:0;margin-top:-6px}.tooltip.left .tooltip-arrow,.tooltip.left .tooltip-arrow:after{border-left-color:#949494;border-width:8px 0 8px 8px;margin-top:-8px;right:0;top:50%}.tooltip.bottom .tooltip-arrow,.tooltip.bottom .tooltip-arrow:after{border-bottom-color:#949494;border-width:0 8px 8px;left:50%;margin-left:-8px;top:0}.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow:after{border-bottom-color:#949494;border-width:0 8px 8px;margin-top:-8px;right:8px;top:0}.tooltip.bottom-right .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow:after{border-bottom-color:#949494;border-width:0 8px 8px;left:8px;margin-top:-8px;top:0}.password-strength{display:block;margin:0 -.3rem 1em;white-space:nowrap}.password-strength.password-strength-too-short .password-strength-item:first-child,.password-strength.password-strength-weak .password-strength-item:first-child,.password-strength.password-strength-weak .password-strength-item:first-child+.password-strength-item{background-color:#e22626}.password-strength.password-strength-fair .password-strength-item:first-child,.password-strength.password-strength-fair .password-strength-item:first-child+.password-strength-item,.password-strength.password-strength-fair .password-strength-item:first-child+.password-strength-item+.password-strength-item{background-color:#ef672f}.password-strength.password-strength-good .password-strength-item:first-child,.password-strength.password-strength-good .password-strength-item:first-child+.password-strength-item,.password-strength.password-strength-good .password-strength-item:first-child+.password-strength-item+.password-strength-item,.password-strength.password-strength-good .password-strength-item:first-child+.password-strength-item+.password-strength-item+.password-strength-item,.password-strength.password-strength-strong .password-strength-item{background-color:#79a22e}.password-strength .password-strength-item{background-color:#ccc;display:inline-block;font-size:0;height:1.4rem;margin-right:.3rem;width:calc(20% - .6rem)}@keyframes progress-bar-stripes{from{background-position:4rem 0}to{background-position:0 0}}.progress{background-color:#fafafa;border:1px solid #ccc;clear:left;height:3rem;margin-bottom:3rem;overflow:hidden}.progress-bar{background-color:#79a22e;color:#fff;float:left;font-size:1.19rem;height:100%;line-height:3rem;text-align:center;transition:width .6s ease;width:0}.progress-bar.active{animation:progress-bar-stripes 2s linear infinite}.progress-bar-text-description{margin-bottom:1.6rem}.progress-bar-text-progress{text-align:right}.page-columns .page-inner-sidebar{margin:0 0 3rem}.page-header{margin-bottom:2.7rem;padding-bottom:2rem;position:relative}.page-header:before{border-bottom:1px solid #e3e3e3;bottom:0;content:'';display:block;height:1px;left:3rem;position:absolute;right:3rem}.container .page-header:before{content:normal}.page-header .message{margin-bottom:1.8rem}.page-header .message+.message{margin-top:-1.5rem}.page-header .admin__action-dropdown,.page-header .search-global-input{transition:none}.container .page-header{margin-bottom:0}.page-title-wrapper{margin-top:1.1rem}.container .page-title-wrapper{background:url(../../pub/images/logo.svg) no-repeat;min-height:41px;padding:4px 0 0 45px}.admin__menu .level-0:first-child>a{margin-top:1.6rem}.admin__menu .level-0:first-child>a:after{top:-1.6rem}.admin__menu .level-0:first-child._active>a:after{display:block}.admin__menu .level-0>a{padding-bottom:1.3rem;padding-top:1.3rem}.admin__menu .level-0>a:before{margin-bottom:.7rem}.admin__menu .item-home>a:before{content:'\e611';font-size:2.3rem;padding-top:-.1rem}.admin__menu .item-component>a:before{content:'\e612'}.admin__menu .item-extension>a:before{content:'\e612'}.admin__menu .item-module>a:before{content:'\e647'}.admin__menu .item-upgrade>a:before{content:'\e614'}.admin__menu .item-system-config>a:before{content:'\e610'}.admin__menu .item-tools>a:before{content:'\e613'}.modal-sub-title{font-size:1.7rem;font-weight:600}.modal-connect-signin .modal-inner-wrap{max-width:80rem}@keyframes ngdialog-fadeout{0%{opacity:1}100%{opacity:0}}@keyframes ngdialog-fadein{0%{opacity:0}100%{opacity:1}}.ngdialog{-webkit-overflow-scrolling:touch;bottom:0;box-sizing:border-box;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:999}.ngdialog *,.ngdialog:after,.ngdialog:before{box-sizing:inherit}.ngdialog.ngdialog-disabled-animation *{animation:none!important}.ngdialog.ngdialog-closing .ngdialog-content,.ngdialog.ngdialog-closing .ngdialog-overlay{-webkit-animation:ngdialog-fadeout .5s;-webkit-backface-visibility:hidden;animation:ngdialog-fadeout .5s}.ngdialog-overlay{-webkit-animation:ngdialog-fadein .5s;-webkit-backface-visibility:hidden;animation:ngdialog-fadein .5s;background:rgba(0,0,0,.4);bottom:0;left:0;position:fixed;right:0;top:0}.ngdialog-content{-webkit-animation:ngdialog-fadein .5s;-webkit-backface-visibility:hidden;animation:ngdialog-fadein .5s}body.ngdialog-open{overflow:hidden}.component-indicator{border-radius:50%;cursor:help;display:inline-block;height:16px;text-align:center;vertical-align:middle;width:16px}.component-indicator::after,.component-indicator::before{background:#fff;display:block;opacity:0;position:absolute;transition:opacity .2s linear .1s;visibility:hidden}.component-indicator::before{border:1px solid #adadad;border-radius:1px;box-shadow:0 0 2px rgba(0,0,0,.4);content:attr(data-label);font-size:1.2rem;margin:30px 0 0 -10px;min-width:50px;padding:4px 5px}.component-indicator::after{border-color:#999;border-style:solid;border-width:1px 0 0 1px;box-shadow:-1px -1px 1px rgba(0,0,0,.1);content:'';height:10px;margin:9px 0 0 5px;-ms-transform:rotate(45deg);transform:rotate(45deg);width:10px}.component-indicator:hover::after,.component-indicator:hover::before{opacity:1;transition:opacity .2s linear;visibility:visible}.component-indicator span{display:block;height:16px;overflow:hidden;width:16px}.component-indicator span:before{content:'';display:block;font-family:Icons;font-size:16px;height:100%;line-height:16px;width:100%}.component-indicator._on{background:#79a22e}.component-indicator._off{background:#e22626}.component-indicator._off span:before{background:#fff;height:4px;margin:8px auto 20px;width:12px}.component-indicator._info{background:0 0}.component-indicator._info span{width:21px}.component-indicator._info span:before{color:#008bdb;content:'\e648';font-family:Icons;font-size:16px}.component-indicator._tooltip{background:0 0;margin:0 0 8px 5px}.component-indicator._tooltip a{width:21px}.component-indicator._tooltip a:hover{text-decoration:none}.component-indicator._tooltip a:before{color:#514943;content:'\e633';font-family:Icons;font-size:16px}.col-manager-item-name .data-grid-data{padding-left:5px}.col-manager-item-name .ng-hide+.data-grid-data{padding-left:24px}.col-manager-item-name ._hide-dependencies,.col-manager-item-name ._show-dependencies{cursor:pointer;padding-left:24px;position:relative}.col-manager-item-name ._hide-dependencies:before,.col-manager-item-name ._show-dependencies:before{display:block;font-family:Icons;font-size:12px;left:0;position:absolute;top:1px}.col-manager-item-name ._show-dependencies:before{content:'\e62b'}.col-manager-item-name ._hide-dependencies:before{content:'\e628'}.col-manager-item-name ._no-dependencies{padding-left:24px}.product-modules-block{font-size:1.2rem;padding:15px 0 0}.col-manager-item-name .product-modules-block{padding-left:1rem}.product-modules-descriprion,.product-modules-title{font-weight:700;margin:0 0 7px}.product-modules-list{font-size:1.1rem;list-style:none;margin:0}.col-manager-item-name .product-modules-list{margin-left:15px}.col-manager-item-name .product-modules-list li{padding:0 0 0 15px;position:relative}.product-modules-list li{margin:0 0 .5rem}.product-modules-list .component-indicator{height:10px;left:0;position:absolute;top:3px;width:10px}.module-summary{white-space:nowrap}.module-summary-title{font-size:2.1rem;margin-right:1rem}.app-updater .nav{display:block;margin-bottom:3.1rem;margin-top:-2.8rem}.app-updater .nav-bar-outer-actions{margin-top:1rem;padding-right:0}.app-updater .nav-bar-outer-actions .btn-wrap-cancel{margin-right:2.6rem}.main{padding-bottom:2rem;padding-top:3rem}.menu-wrapper .logo-static{pointer-events:none}.header{display:none}.header .logo{float:left;height:4.1rem;width:3.5rem}.header-title{font-size:2.8rem;letter-spacing:.02em;line-height:1.4;margin:2.5rem 0 3.5rem 5rem}.page-title{margin-bottom:1rem}.page-sub-title{font-size:2rem}.accent-box{margin-bottom:2rem}.accent-box .btn-prime{margin-top:1.5rem}.spinner.side{float:left;font-size:2.4rem;margin-left:2rem;margin-top:-5px}.page-landing{margin:7.6% auto 0;max-width:44rem;text-align:center}.page-landing .logo{height:5.6rem;margin-bottom:2rem;width:19.2rem}.page-landing .text-version{margin-bottom:3rem}.page-landing .text-welcome{margin-bottom:6.5rem}.page-landing .text-terms{margin-bottom:2.5rem;text-align:center}.page-landing .btn-submit,.page-license .license-text{margin-bottom:2rem}.page-license .page-license-footer{text-align:right}.readiness-check-item{margin-bottom:4rem;min-height:2.5rem}.readiness-check-item .spinner{float:left;font-size:2.5rem;margin:-.4rem 0 0 1.7rem}.readiness-check-title{font-size:1.4rem;font-weight:700;margin-bottom:.1rem;margin-left:5.7rem}.readiness-check-content{margin-left:5.7rem;margin-right:22rem;position:relative}.readiness-check-content .readiness-check-title{margin-left:0}.readiness-check-content .list{margin-top:-.3rem}.readiness-check-side{left:100%;padding-left:2.4rem;position:absolute;top:0;width:22rem}.readiness-check-side .side-title{margin-bottom:0}.readiness-check-icon{float:left;margin-left:1.7rem;margin-top:.3rem}.extensions-information{margin-bottom:5rem}.extensions-information h3{font-size:1.4rem;margin-bottom:1.3rem}.extensions-information .message{margin-bottom:2.5rem}.extensions-information .message:before{margin-top:0;top:1.8rem}.extensions-information .extensions-container{padding:0 2rem}.extensions-information .list{margin-bottom:1rem}.extensions-information .list select{cursor:pointer}.extensions-information .list select:disabled{background:#ccc;cursor:default}.extensions-information .list .extension-delete{font-size:1.7rem;padding-top:0}.delete-modal-wrap{padding:0 4% 4rem}.delete-modal-wrap h3{font-size:3.4rem;display:inline-block;font-weight:300;margin:0 0 2rem;padding:.9rem 0 0;vertical-align:top}.delete-modal-wrap .actions{padding:3rem 0 0}.page-web-configuration .form-el-insider-wrap{width:auto}.page-web-configuration .form-el-insider{width:15.4rem}.page-web-configuration .form-el-insider-input .form-el-input{width:16.5rem}.customize-your-store .advanced-modules-count,.customize-your-store .advanced-modules-select{padding-left:1.5rem}.customize-your-store .customize-your-store-advanced{min-width:0}.customize-your-store .message-error:before{margin-top:0;top:1.8rem}.customize-your-store .message-error a{color:#333;text-decoration:underline}.customize-your-store .message-error .form-label:before{background:#fff}.customize-your-store .customize-database-clean p{margin-top:2.5rem}.content-install{margin-bottom:2rem}.console{border:1px solid #ccc;font-family:'Courier New',Courier,monospace;font-weight:300;height:20rem;margin:1rem 0 2rem;overflow-y:auto;padding:1.5rem 2rem 2rem;resize:vertical}.console .text-danger{color:#e22626}.console .text-success{color:#090}.console .hidden{display:none}.content-success .btn-prime{margin-top:1.5rem}.jumbo-title{font-size:3.6rem}.jumbo-title .jumbo-icon{font-size:3.8rem;margin-right:.25em;position:relative;top:.15em}.install-database-clean{margin-top:4rem}.install-database-clean .btn{margin-right:1rem}.page-sub-title{margin-bottom:2.1rem;margin-top:3rem}.multiselect-custom{max-width:71.1rem}.content-install{margin-top:3.7rem}.home-page-inner-wrap{margin:0 auto;max-width:91rem}.setup-home-title{margin-bottom:3.9rem;padding-top:1.8rem;text-align:center}.setup-home-item{background-color:#fafafa;border:1px solid #ccc;color:#333;display:block;margin-bottom:2rem;margin-left:1.3rem;margin-right:1.3rem;min-height:30rem;padding:2rem;text-align:center}.setup-home-item:hover{border-color:#8c8c8c;color:#333;text-decoration:none;transition:border-color .1s linear}.setup-home-item:active{-ms-transform:scale(0.99);transform:scale(0.99)}.setup-home-item:before{display:block;font-size:7rem;margin-bottom:3.3rem;margin-top:4rem}.setup-home-item-component:before,.setup-home-item-extension:before{content:'\e612'}.setup-home-item-module:before{content:'\e647'}.setup-home-item-upgrade:before{content:'\e614'}.setup-home-item-configuration:before{content:'\e610'}.setup-home-item-title{display:block;font-size:1.8rem;letter-spacing:.025em;margin-bottom:1rem}.setup-home-item-description{display:block}.extension-manager-wrap{border:1px solid #bbb;margin:0 0 4rem}.extension-manager-account{font-size:2.1rem;display:inline-block;font-weight:400}.extension-manager-title{font-size:3.2rem;background-color:#f8f8f8;border-bottom:1px solid #e3e3e3;color:#41362f;font-weight:600;line-height:1.2;padding:2rem}.extension-manager-content{padding:2.5rem 2rem 2rem}.extension-manager-items{list-style:none;margin:0;text-align:center}.extension-manager-items .btn{border:1px solid #adadad;display:block;margin:1rem auto 0}.extension-manager-items .item-title{font-size:2.1rem;display:inline-block;text-align:left}.extension-manager-items .item-number{font-size:4.1rem;display:inline-block;line-height:.8;margin:0 5px 1.5rem 0;vertical-align:top}.extension-manager-items .item-date{font-size:2.6rem;margin-top:1px}.extension-manager-items .item-date-title{font-size:1.5rem}.extension-manager-items .item-install{margin:0 0 2rem}.sync-login-wrap{padding:0 10% 4rem}.sync-login-wrap .legend{font-size:2.6rem;color:#eb5202;float:left;font-weight:300;line-height:1.2;margin:-1rem 0 2.5rem;position:static;width:100%}.sync-login-wrap .legend._hidden{display:none}.sync-login-wrap .login-header{font-size:3.4rem;font-weight:300;margin:0 0 2rem}.sync-login-wrap .login-header span{display:inline-block;padding:.9rem 0 0;vertical-align:top}.sync-login-wrap h4{font-size:1.4rem;margin:0 0 2rem}.sync-login-wrap .sync-login-steps{margin:0 0 2rem 1.5rem}.sync-login-wrap .sync-login-steps li{padding:0 0 0 1rem}.sync-login-wrap .form-row .form-label{display:inline-block}.sync-login-wrap .form-row .form-label.required{padding-left:1.5rem}.sync-login-wrap .form-row .form-label.required:after{left:0;position:absolute;right:auto}.sync-login-wrap .form-row{max-width:28rem}.sync-login-wrap .form-actions{display:table;margin-top:-1.3rem}.sync-login-wrap .form-actions .links{display:table-header-group}.sync-login-wrap .form-actions .actions{padding:3rem 0 0}@media all and (max-width:1047px){.admin__menu .submenu li{min-width:19.8rem}.nav{padding-bottom:5.38rem;padding-left:1.5rem;text-align:center}.nav-bar{display:inline-block;float:none;margin-right:0;vertical-align:top}.nav .btn-group,.nav-bar-outer-actions{display:inline-block;float:none;margin-top:-8.48rem;text-align:center;vertical-align:top;width:100%}.nav-bar-outer-actions{padding-right:0}.nav-bar-outer-actions .outer-actions-inner-wrap{display:inline-block}.app-updater .nav{padding-bottom:1.7rem}.app-updater .nav-bar-outer-actions{margin-top:2rem}}@media all and (min-width:768px){.page-layout-admin-2columns-left .page-columns{margin-left:-30px}.page-layout-admin-2columns-left .page-columns:after{clear:both;content:'';display:table}.page-layout-admin-2columns-left .page-columns .main-col{width:calc((100%) * .75 - 30px);float:right}.page-layout-admin-2columns-left .page-columns .side-col{width:calc((100%) * .25 - 30px);float:left;margin-left:30px}.col-m-1,.col-m-10,.col-m-11,.col-m-12,.col-m-2,.col-m-3,.col-m-4,.col-m-5,.col-m-6,.col-m-7,.col-m-8,.col-m-9{float:left}.col-m-12{width:100%}.col-m-11{width:91.66666667%}.col-m-10{width:83.33333333%}.col-m-9{width:75%}.col-m-8{width:66.66666667%}.col-m-7{width:58.33333333%}.col-m-6{width:50%}.col-m-5{width:41.66666667%}.col-m-4{width:33.33333333%}.col-m-3{width:25%}.col-m-2{width:16.66666667%}.col-m-1{width:8.33333333%}.col-m-pull-12{right:100%}.col-m-pull-11{right:91.66666667%}.col-m-pull-10{right:83.33333333%}.col-m-pull-9{right:75%}.col-m-pull-8{right:66.66666667%}.col-m-pull-7{right:58.33333333%}.col-m-pull-6{right:50%}.col-m-pull-5{right:41.66666667%}.col-m-pull-4{right:33.33333333%}.col-m-pull-3{right:25%}.col-m-pull-2{right:16.66666667%}.col-m-pull-1{right:8.33333333%}.col-m-pull-0{right:auto}.col-m-push-12{left:100%}.col-m-push-11{left:91.66666667%}.col-m-push-10{left:83.33333333%}.col-m-push-9{left:75%}.col-m-push-8{left:66.66666667%}.col-m-push-7{left:58.33333333%}.col-m-push-6{left:50%}.col-m-push-5{left:41.66666667%}.col-m-push-4{left:33.33333333%}.col-m-push-3{left:25%}.col-m-push-2{left:16.66666667%}.col-m-push-1{left:8.33333333%}.col-m-push-0{left:auto}.col-m-offset-12{margin-left:100%}.col-m-offset-11{margin-left:91.66666667%}.col-m-offset-10{margin-left:83.33333333%}.col-m-offset-9{margin-left:75%}.col-m-offset-8{margin-left:66.66666667%}.col-m-offset-7{margin-left:58.33333333%}.col-m-offset-6{margin-left:50%}.col-m-offset-5{margin-left:41.66666667%}.col-m-offset-4{margin-left:33.33333333%}.col-m-offset-3{margin-left:25%}.col-m-offset-2{margin-left:16.66666667%}.col-m-offset-1{margin-left:8.33333333%}.col-m-offset-0{margin-left:0}.page-columns{margin-left:-30px}.page-columns:after{clear:both;content:'';display:table}.page-columns .page-inner-content{width:calc((100%) * .75 - 30px);float:right}.page-columns .page-inner-sidebar{width:calc((100%) * .25 - 30px);float:left;margin-left:30px}}@media all and (min-width:1048px){.col-l-1,.col-l-10,.col-l-11,.col-l-12,.col-l-2,.col-l-3,.col-l-4,.col-l-5,.col-l-6,.col-l-7,.col-l-8,.col-l-9{float:left}.col-l-12{width:100%}.col-l-11{width:91.66666667%}.col-l-10{width:83.33333333%}.col-l-9{width:75%}.col-l-8{width:66.66666667%}.col-l-7{width:58.33333333%}.col-l-6{width:50%}.col-l-5{width:41.66666667%}.col-l-4{width:33.33333333%}.col-l-3{width:25%}.col-l-2{width:16.66666667%}.col-l-1{width:8.33333333%}.col-l-pull-12{right:100%}.col-l-pull-11{right:91.66666667%}.col-l-pull-10{right:83.33333333%}.col-l-pull-9{right:75%}.col-l-pull-8{right:66.66666667%}.col-l-pull-7{right:58.33333333%}.col-l-pull-6{right:50%}.col-l-pull-5{right:41.66666667%}.col-l-pull-4{right:33.33333333%}.col-l-pull-3{right:25%}.col-l-pull-2{right:16.66666667%}.col-l-pull-1{right:8.33333333%}.col-l-pull-0{right:auto}.col-l-push-12{left:100%}.col-l-push-11{left:91.66666667%}.col-l-push-10{left:83.33333333%}.col-l-push-9{left:75%}.col-l-push-8{left:66.66666667%}.col-l-push-7{left:58.33333333%}.col-l-push-6{left:50%}.col-l-push-5{left:41.66666667%}.col-l-push-4{left:33.33333333%}.col-l-push-3{left:25%}.col-l-push-2{left:16.66666667%}.col-l-push-1{left:8.33333333%}.col-l-push-0{left:auto}.col-l-offset-12{margin-left:100%}.col-l-offset-11{margin-left:91.66666667%}.col-l-offset-10{margin-left:83.33333333%}.col-l-offset-9{margin-left:75%}.col-l-offset-8{margin-left:66.66666667%}.col-l-offset-7{margin-left:58.33333333%}.col-l-offset-6{margin-left:50%}.col-l-offset-5{margin-left:41.66666667%}.col-l-offset-4{margin-left:33.33333333%}.col-l-offset-3{margin-left:25%}.col-l-offset-2{margin-left:16.66666667%}.col-l-offset-1{margin-left:8.33333333%}.col-l-offset-0{margin-left:0}}@media all and (min-width:1440px){.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{float:left}.col-xl-12{width:100%}.col-xl-11{width:91.66666667%}.col-xl-10{width:83.33333333%}.col-xl-9{width:75%}.col-xl-8{width:66.66666667%}.col-xl-7{width:58.33333333%}.col-xl-6{width:50%}.col-xl-5{width:41.66666667%}.col-xl-4{width:33.33333333%}.col-xl-3{width:25%}.col-xl-2{width:16.66666667%}.col-xl-1{width:8.33333333%}.col-xl-pull-12{right:100%}.col-xl-pull-11{right:91.66666667%}.col-xl-pull-10{right:83.33333333%}.col-xl-pull-9{right:75%}.col-xl-pull-8{right:66.66666667%}.col-xl-pull-7{right:58.33333333%}.col-xl-pull-6{right:50%}.col-xl-pull-5{right:41.66666667%}.col-xl-pull-4{right:33.33333333%}.col-xl-pull-3{right:25%}.col-xl-pull-2{right:16.66666667%}.col-xl-pull-1{right:8.33333333%}.col-xl-pull-0{right:auto}.col-xl-push-12{left:100%}.col-xl-push-11{left:91.66666667%}.col-xl-push-10{left:83.33333333%}.col-xl-push-9{left:75%}.col-xl-push-8{left:66.66666667%}.col-xl-push-7{left:58.33333333%}.col-xl-push-6{left:50%}.col-xl-push-5{left:41.66666667%}.col-xl-push-4{left:33.33333333%}.col-xl-push-3{left:25%}.col-xl-push-2{left:16.66666667%}.col-xl-push-1{left:8.33333333%}.col-xl-push-0{left:auto}.col-xl-offset-12{margin-left:100%}.col-xl-offset-11{margin-left:91.66666667%}.col-xl-offset-10{margin-left:83.33333333%}.col-xl-offset-9{margin-left:75%}.col-xl-offset-8{margin-left:66.66666667%}.col-xl-offset-7{margin-left:58.33333333%}.col-xl-offset-6{margin-left:50%}.col-xl-offset-5{margin-left:41.66666667%}.col-xl-offset-4{margin-left:33.33333333%}.col-xl-offset-3{margin-left:25%}.col-xl-offset-2{margin-left:16.66666667%}.col-xl-offset-1{margin-left:8.33333333%}.col-xl-offset-0{margin-left:0}}@media all and (max-width:767px){.abs-clearer-mobile:after,.nav-bar:after{clear:both;content:'';display:table}.list-definition>dt{float:none}.list-definition>dd{margin-left:0}.form-row .form-label{text-align:left}.form-row .form-label.required:after{position:static}.nav{padding-bottom:0;padding-left:0;padding-right:0}.nav-bar-outer-actions{margin-top:0}.nav-bar{display:block;margin-bottom:0;margin-left:auto;margin-right:auto;width:30.9rem}.nav-bar:before{display:none}.nav-bar>li{float:left;min-height:9rem}.nav-bar>li:after{display:none}.nav-bar>li:nth-child(4n){clear:both}.nav-bar a{line-height:1.4}.tooltip{display:none!important}.readiness-check-content{margin-right:2rem}.readiness-check-side{padding:2rem 0;position:static}.form-el-insider,.form-el-insider-wrap,.page-web-configuration .form-el-insider-input,.page-web-configuration .form-el-insider-input .form-el-input{display:block;width:100%}}@media all and (max-width:479px){.nav-bar{width:23.175rem}.nav-bar>li{width:7.725rem}.nav .btn-group .btn-wrap-try-again,.nav-bar-outer-actions .btn-wrap-try-again{clear:both;display:block;float:none;margin-left:auto;margin-right:auto;margin-top:1rem;padding-top:1rem}} +.abs-action-delete,.abs-icon,.action-close:before,.action-next:before,.action-previous:before,.admin-user .admin__action-dropdown:before,.admin__action-multiselect-dropdown:before,.admin__action-multiselect-search-label:before,.admin__control-checkbox+label:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:before,.admin__control-table .action-delete:before,.admin__current-filters-list .action-remove:before,.admin__data-grid-action-bookmarks .action-delete:before,.admin__data-grid-action-bookmarks .action-edit:before,.admin__data-grid-action-bookmarks .action-submit:before,.admin__data-grid-action-bookmarks .admin__action-dropdown:before,.admin__data-grid-action-columns .admin__action-dropdown:before,.admin__data-grid-action-export .admin__action-dropdown:before,.admin__field-fallback-reset:before,.admin__menu .level-0>a:before,.admin__page-nav-item-message .admin__page-nav-item-message-icon,.admin__page-nav-title._collapsible:after,.data-grid-filters-action-wrap .action-default:before,.data-grid-row-changed:after,.data-grid-row-parent>td .data-grid-checkbox-cell-inner:before,.data-grid-search-control-wrap .action-submit:before,.extensions-information .list .extension-delete,.icon-failed:before,.icon-success:before,.notifications-action:before,.notifications-close:before,.page-actions .page-actions-buttons>button.action-back:before,.page-actions .page-actions-buttons>button.back:before,.page-actions>button.action-back:before,.page-actions>button.back:before,.page-title-jumbo-success:before,.search-global-label:before,.selectmenu .action-delete:before,.selectmenu .action-edit:before,.selectmenu .action-save:before,.setup-home-item:before,.sticky-header .data-grid-search-control-wrap .data-grid-search-label:before,.store-switcher .dropdown-menu .dropdown-toolbar a:before,.tooltip .help a:before,.tooltip .help span:before{-webkit-font-smoothing:antialiased;font-family:Icons;font-style:normal;font-weight:400;line-height:1;speak:none}.validation-symbol:after{color:#e22626;content:'*';font-weight:400;margin-left:3px}.abs-modal-overlay,.modals-overlay{background:rgba(0,0,0,.35);bottom:0;left:0;position:fixed;right:0;top:0}.abs-action-delete>span,.abs-visually-hidden,.action-multicheck-wrap .action-multicheck-toggle>span,.admin__actions-switch-checkbox,.admin__control-fields .admin__field:nth-child(n+2):not(.admin__field-option):not(.admin__field-group-show-label)>.admin__field-label,.admin__field-tooltip .admin__field-tooltip-action span,.customize-your-store .customize-your-store-default .legend,.extensions-information .list .extension-delete>span,.form-el-checkbox,.form-el-radio,.selectmenu .action-delete>span,.selectmenu .action-edit>span,.selectmenu .action-save>span,.selectmenu-toggle span,.tooltip .help a span,.tooltip .help span span,[class*=admin__control-grouped]>.admin__field:nth-child(n+2):not(.admin__field-option):not(.admin__field-group-show-label):not(.admin__field-date)>.admin__field-label{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.abs-visually-hidden-reset,.admin__field-group-columns>.admin__field:nth-child(n+2):not(.admin__field-option):not(.admin__field-group-show-label):not(.admin__field-date)>.admin__field-label[class]{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.abs-clearfix:after,.abs-clearfix:before,.action-multicheck-wrap:after,.action-multicheck-wrap:before,.actions-split:after,.actions-split:before,.admin__control-table-pagination:after,.admin__control-table-pagination:before,.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content:after,.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content:before,.admin__data-grid-filters-footer:after,.admin__data-grid-filters-footer:before,.admin__data-grid-filters:after,.admin__data-grid-filters:before,.admin__data-grid-header-row:after,.admin__data-grid-header-row:before,.admin__field-complex:after,.admin__field-complex:before,.modal-slide .magento-message .insert-title-inner:after,.modal-slide .magento-message .insert-title-inner:before,.modal-slide .main-col .insert-title-inner:after,.modal-slide .main-col .insert-title-inner:before,.page-actions._fixed:after,.page-actions._fixed:before,.page-content:after,.page-content:before,.page-header-actions:after,.page-header-actions:before,.page-main-actions:not(._hidden):after,.page-main-actions:not(._hidden):before{content:'';display:table}.abs-clearfix:after,.action-multicheck-wrap:after,.actions-split:after,.admin__control-table-pagination:after,.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content:after,.admin__data-grid-filters-footer:after,.admin__data-grid-filters:after,.admin__data-grid-header-row:after,.admin__field-complex:after,.modal-slide .magento-message .insert-title-inner:after,.modal-slide .main-col .insert-title-inner:after,.page-actions._fixed:after,.page-content:after,.page-header-actions:after,.page-main-actions:not(._hidden):after{clear:both}.abs-list-reset-styles{margin:0;padding:0;list-style:none}.abs-draggable-handle,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .draggable-handle,.admin__control-table .draggable-handle,.data-grid .data-grid-draggable-row-cell .draggable-handle{cursor:-webkit-grab;cursor:move;font-size:0;margin-top:-4px;padding:0 1rem 0 0;vertical-align:middle;display:inline-block;text-decoration:none}.abs-draggable-handle:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .draggable-handle:before,.admin__control-table .draggable-handle:before,.data-grid .data-grid-draggable-row-cell .draggable-handle:before{-webkit-font-smoothing:antialiased;font-size:1.8rem;line-height:inherit;color:#9e9e9e;content:'\e617';font-family:Icons;vertical-align:middle;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.abs-draggable-handle:hover:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .draggable-handle:hover:before,.admin__control-table .draggable-handle:hover:before,.data-grid .data-grid-draggable-row-cell .draggable-handle:hover:before{color:#858585}.abs-config-scope-label,.admin__field:not(.admin__field-option)>.admin__field-label span[data-config-scope]:before{bottom:-1.3rem;color:gray;content:attr(data-config-scope);font-size:1.1rem;font-weight:400;min-width:15rem;position:absolute;right:0;text-transform:lowercase}.abs-word-wrap,.admin__field:not(.admin__field-option)>.admin__field-label{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-word;-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;box-sizing:border-box}*,:after,:before{box-sizing:inherit}:focus{box-shadow:none;outline:0}._keyfocus :focus{box-shadow:0 0 0 1px #008bdb}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}embed,img,object,video{max-width:100%}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/light/opensans-300.eot);src:url(../fonts/opensans/light/opensans-300.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/light/opensans-300.woff2) format('woff2'),url(../fonts/opensans/light/opensans-300.woff) format('woff'),url(../fonts/opensans/light/opensans-300.ttf) format('truetype'),url('../fonts/opensans/light/opensans-300.svg#Open Sans') format('svg');font-weight:300;font-style:normal}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/regular/opensans-400.eot);src:url(../fonts/opensans/regular/opensans-400.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/regular/opensans-400.woff2) format('woff2'),url(../fonts/opensans/regular/opensans-400.woff) format('woff'),url(../fonts/opensans/regular/opensans-400.ttf) format('truetype'),url('../fonts/opensans/regular/opensans-400.svg#Open Sans') format('svg');font-weight:400;font-style:normal}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/semibold/opensans-600.eot);src:url(../fonts/opensans/semibold/opensans-600.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/semibold/opensans-600.woff2) format('woff2'),url(../fonts/opensans/semibold/opensans-600.woff) format('woff'),url(../fonts/opensans/semibold/opensans-600.ttf) format('truetype'),url('../fonts/opensans/semibold/opensans-600.svg#Open Sans') format('svg');font-weight:600;font-style:normal}@font-face{font-family:'Open Sans';src:url(../fonts/opensans/bold/opensans-700.eot);src:url(../fonts/opensans/bold/opensans-700.eot?#iefix) format('embedded-opentype'),url(../fonts/opensans/bold/opensans-700.woff2) format('woff2'),url(../fonts/opensans/bold/opensans-700.woff) format('woff'),url(../fonts/opensans/bold/opensans-700.ttf) format('truetype'),url('../fonts/opensans/bold/opensans-700.svg#Open Sans') format('svg');font-weight:700;font-style:normal}html{font-size:62.5%}body{color:#333;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.36;font-size:1.4rem}h1{margin:0 0 2rem;color:#41362f;font-weight:400;line-height:1.2;font-size:2.8rem}h2{margin:0 0 2rem;color:#41362f;font-weight:400;line-height:1.2;font-size:2rem}h3{margin:0 0 2rem;color:#41362f;font-weight:600;line-height:1.2;font-size:1.7rem}h4,h5,h6{font-weight:600;margin-top:0}p{margin:0 0 1em}small{font-size:1.2rem}a{color:#008bdb;text-decoration:none}a:hover{color:#0fa7ff;text-decoration:underline}dl,ol,ul{padding-left:0}nav ol,nav ul{list-style:none;margin:0;padding:0}html{height:100%}body{background-color:#fff;min-height:100%;min-width:102.4rem}.page-wrapper{background-color:#fff;display:inline-block;margin-left:-4px;vertical-align:top;width:calc(100% - 8.8rem)}.page-content{padding-bottom:3rem;padding-left:3rem;padding-right:3rem}.notices-wrapper{margin:0 3rem}.notices-wrapper .messages{margin-bottom:0}.row{margin-left:0;margin-right:0}.row:after{clear:both;content:'';display:table}.col-l-1,.col-l-10,.col-l-11,.col-l-12,.col-l-2,.col-l-3,.col-l-4,.col-l-5,.col-l-6,.col-l-7,.col-l-8,.col-l-9,.col-m-1,.col-m-10,.col-m-11,.col-m-12,.col-m-2,.col-m-3,.col-m-4,.col-m-5,.col-m-6,.col-m-7,.col-m-8,.col-m-9,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{min-height:1px;padding-left:0;padding-right:0;position:relative}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}.row-gutter{margin-left:-1.5rem;margin-right:-1.5rem}.row-gutter>[class*=col-]{padding-left:1.5rem;padding-right:1.5rem}.abs-clearer:after,.extension-manager-content:after,.extension-manager-title:after,.form-row:after,.header:after,.nav:after,body:after{clear:both;content:'';display:table}.ng-cloak{display:none!important}.hide.hide{display:none}.show.show{display:block}.text-center{text-align:center}.text-right{text-align:right}@font-face{font-family:Icons;src:url(../fonts/icons/icons.eot);src:url(../fonts/icons/icons.eot?#iefix) format('embedded-opentype'),url(../fonts/icons/icons.woff2) format('woff2'),url(../fonts/icons/icons.woff) format('woff'),url(../fonts/icons/icons.ttf) format('truetype'),url(../fonts/icons/icons.svg#Icons) format('svg');font-weight:400;font-style:normal}[class*=icon-]{display:inline-block;line-height:1}.icon-failed:before,.icon-success:before,[class*=icon-]:after{font-family:Icons}.icon-success{color:#79a22e}.icon-success:before{content:'\e62d'}.icon-failed{color:#e22626}.icon-failed:before{content:'\e632'}.icon-success-thick:after{content:'\e62d'}.icon-collapse:after{content:'\e615'}.icon-failed-thick:after{content:'\e632'}.icon-expand:after{content:'\e616'}.icon-warning:after{content:'\e623'}.icon-failed-round,.icon-success-round{border-radius:100%;color:#fff;font-size:2.5rem;height:1em;position:relative;text-align:center;width:1em}.icon-failed-round:after,.icon-success-round:after{bottom:0;font-size:.5em;left:0;position:absolute;right:0;top:.45em}.icon-success-round{background-color:#79a22e}.icon-success-round:after{content:'\e62d'}.icon-failed-round{background-color:#e22626}.icon-failed-round:after{content:'\e632'}dl,ol,ul{margin-top:0}.list{padding-left:0}.list>li{display:block;margin-bottom:.75em;position:relative}.list>li>.icon-failed,.list>li>.icon-success{font-size:1.6em;left:-.1em;position:absolute;top:0}.list>li>.icon-success{color:#79a22e}.list>li>.icon-failed{color:#e22626}.list-item-failed,.list-item-icon,.list-item-success,.list-item-warning{padding-left:3.5rem}.list-item-failed:before,.list-item-success:before,.list-item-warning:before{left:-.1em;position:absolute}.list-item-success:before{color:#79a22e}.list-item-failed:before{color:#e22626}.list-item-warning:before{color:#ef672f}.list-definition{margin:0 0 3rem;padding:0}.list-definition>dt{clear:left;float:left}.list-definition>dd{margin-bottom:1em;margin-left:20rem}.btn-wrap{margin:0 auto}.btn-wrap .btn{width:100%}.btn{background:#e3e3e3;border:none;color:#514943;display:inline-block;font-size:1.6rem;font-weight:600;padding:.45em .9em;text-align:center}.btn:hover{background-color:#dbdbdb;color:#514943;text-decoration:none}.btn:active{background-color:#d6d6d6}.btn.disabled,.btn[disabled]{cursor:default;opacity:.5;pointer-events:none}.ie9 .btn.disabled,.ie9 .btn[disabled]{background-color:#f0f0f0;opacity:1;text-shadow:none}.btn-large{padding:.75em 1.25em}.btn-medium{font-size:1.4rem;padding:.5em 1.5em .6em}.btn-link{background-color:transparent;border:none;color:#008bdb;font-family:1.6rem;font-size:1.5rem}.btn-link:active,.btn-link:focus,.btn-link:hover{background-color:transparent;color:#0fa7ff}.btn-prime{background-color:#eb5202;color:#fff;text-shadow:1px 1px 0 rgba(0,0,0,.25)}.btn-prime:focus,.btn-prime:hover{background-color:#f65405;background-repeat:repeat-x;background-image:linear-gradient(to right,#e04f00 0,#f65405 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e04f00', endColorstr='#f65405', GradientType=1);color:#fff}.btn-prime:active{background-color:#e04f00;background-repeat:repeat-x;background-image:linear-gradient(to right,#f65405 0,#e04f00 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f65405', endColorstr='#e04f00', GradientType=1);color:#fff}.ie9 .btn-prime.disabled,.ie9 .btn-prime[disabled]{background-color:#fd6e23}.ie9 .btn-prime.disabled:active,.ie9 .btn-prime.disabled:hover,.ie9 .btn-prime[disabled]:active,.ie9 .btn-prime[disabled]:hover{background-color:#fd6e23;-webkit-filter:none;filter:none}.btn-secondary{background-color:#514943;color:#fff}.btn-secondary:hover{background-color:#5f564f;color:#fff}.btn-secondary:active,.btn-secondary:focus{background-color:#574e48;color:#fff}.ie9 .btn-secondary.disabled,.ie9 .btn-secondary[disabled]{background-color:#514943}.ie9 .btn-secondary.disabled:active,.ie9 .btn-secondary[disabled]:active{background-color:#514943;-webkit-filter:none;filter:none}[class*=btn-wrap-triangle]{overflow:hidden;position:relative}[class*=btn-wrap-triangle] .btn:after{border-style:solid;content:'';height:0;position:absolute;top:0;width:0}.btn-wrap-triangle-right{display:inline-block;padding-right:1.74rem;position:relative}.btn-wrap-triangle-right .btn{text-indent:.92rem}.btn-wrap-triangle-right .btn:after{border-color:transparent transparent transparent #e3e3e3;border-width:1.84rem 0 1.84rem 1.84rem;left:100%;margin-left:-1.74rem}.btn-wrap-triangle-right .btn:focus:after,.btn-wrap-triangle-right .btn:hover:after{border-left-color:#dbdbdb}.btn-wrap-triangle-right .btn:active:after{border-left-color:#d6d6d6}.btn-wrap-triangle-right .btn:not(.disabled):active,.btn-wrap-triangle-right .btn:not([disabled]):active{left:1px}.ie9 .btn-wrap-triangle-right .btn.disabled:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:after{border-color:transparent transparent transparent #f0f0f0}.ie9 .btn-wrap-triangle-right .btn.disabled:active:after,.ie9 .btn-wrap-triangle-right .btn.disabled:focus:after,.ie9 .btn-wrap-triangle-right .btn.disabled:hover:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:active:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:focus:after,.ie9 .btn-wrap-triangle-right .btn[disabled]:hover:after{border-left-color:#f0f0f0}.btn-wrap-triangle-right .btn-prime:after{border-color:transparent transparent transparent #eb5202}.btn-wrap-triangle-right .btn-prime:focus:after,.btn-wrap-triangle-right .btn-prime:hover:after{border-left-color:#f65405}.btn-wrap-triangle-right .btn-prime:active:after{border-left-color:#e04f00}.btn-wrap-triangle-right .btn-prime:not(.disabled):active,.btn-wrap-triangle-right .btn-prime:not([disabled]):active{left:1px}.ie9 .btn-wrap-triangle-right .btn-prime.disabled:after,.ie9 .btn-wrap-triangle-right .btn-prime[disabled]:after{border-color:transparent transparent transparent #fd6e23}.ie9 .btn-wrap-triangle-right .btn-prime.disabled:active:after,.ie9 .btn-wrap-triangle-right .btn-prime.disabled:hover:after,.ie9 .btn-wrap-triangle-right .btn-prime[disabled]:active:after,.ie9 .btn-wrap-triangle-right .btn-prime[disabled]:hover:after{border-left-color:#fd6e23}.btn-wrap-triangle-left{display:inline-block;padding-left:1.74rem}.btn-wrap-triangle-left .btn{text-indent:-.92rem}.btn-wrap-triangle-left .btn:after{border-color:transparent #e3e3e3 transparent transparent;border-width:1.84rem 1.84rem 1.84rem 0;margin-right:-1.74rem;right:100%}.btn-wrap-triangle-left .btn:focus:after,.btn-wrap-triangle-left .btn:hover:after{border-right-color:#dbdbdb}.btn-wrap-triangle-left .btn:active:after{border-right-color:#d6d6d6}.btn-wrap-triangle-left .btn:not(.disabled):active,.btn-wrap-triangle-left .btn:not([disabled]):active{right:1px}.ie9 .btn-wrap-triangle-left .btn.disabled:after,.ie9 .btn-wrap-triangle-left .btn[disabled]:after{border-color:transparent #f0f0f0 transparent transparent}.ie9 .btn-wrap-triangle-left .btn.disabled:active:after,.ie9 .btn-wrap-triangle-left .btn.disabled:hover:after,.ie9 .btn-wrap-triangle-left .btn[disabled]:active:after,.ie9 .btn-wrap-triangle-left .btn[disabled]:hover:after{border-right-color:#f0f0f0}.btn-wrap-triangle-left .btn-prime:after{border-color:transparent #eb5202 transparent transparent}.btn-wrap-triangle-left .btn-prime:focus:after,.btn-wrap-triangle-left .btn-prime:hover:after{border-right-color:#e04f00}.btn-wrap-triangle-left .btn-prime:active:after{border-right-color:#f65405}.btn-wrap-triangle-left .btn-prime:not(.disabled):active,.btn-wrap-triangle-left .btn-prime:not([disabled]):active{right:1px}.ie9 .btn-wrap-triangle-left .btn-prime.disabled:after,.ie9 .btn-wrap-triangle-left .btn-prime[disabled]:after{border-color:transparent #fd6e23 transparent transparent}.ie9 .btn-wrap-triangle-left .btn-prime.disabled:active:after,.ie9 .btn-wrap-triangle-left .btn-prime.disabled:hover:after,.ie9 .btn-wrap-triangle-left .btn-prime[disabled]:active:after,.ie9 .btn-wrap-triangle-left .btn-prime[disabled]:hover:after{border-right-color:#fd6e23}.btn-expand{background-color:transparent;border:none;color:#303030;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:700;padding:0;position:relative}.btn-expand.expanded:after{border-color:transparent transparent #303030;border-width:0 .285em .36em}.btn-expand.expanded:hover:after{border-color:transparent transparent #3d3d3d}.btn-expand:hover{background-color:transparent;border:none;color:#3d3d3d}.btn-expand:hover:after{border-color:#3d3d3d transparent transparent}.btn-expand:after{border-color:#303030 transparent transparent;border-style:solid;border-width:.36em .285em 0;content:'';height:0;left:100%;margin-left:.5em;margin-top:-.18em;position:absolute;top:50%;width:0}[class*=col-] .form-el-input,[class*=col-] .form-el-select{width:100%}.form-fieldset{border:none;margin:0 0 1em;padding:0}.form-row{margin-bottom:2.2rem}.form-row .form-row{margin-bottom:.4rem}.form-row .form-label{display:block;font-weight:600;padding:.6rem 2.1em 0 0;text-align:right}.form-row .form-label.required{position:relative}.form-row .form-label.required:after{color:#eb5202;content:'*';font-size:1.15em;position:absolute;right:.7em;top:.5em}.form-row .form-el-checkbox+.form-label:before,.form-row .form-el-radio+.form-label:before{top:.7rem}.form-row .form-el-checkbox+.form-label:after,.form-row .form-el-radio+.form-label:after{top:1.1rem}.form-row.form-row-text{padding-top:.6rem}.form-row.form-row-text .action-sign-out{font-size:1.2rem;margin-left:1rem}.form-note{font-size:1.2rem;font-weight:600;margin-top:1rem}.form-el-dummy{display:none}.fieldset{border:0;margin:0;min-width:0;padding:0}input:not([disabled]):focus,textarea:not([disabled]):focus{box-shadow:none}.form-el-input{border:1px solid #adadad;color:#303030;padding:.35em .55em .5em}.form-el-input:hover{border-color:#949494}.form-el-input:focus{border-color:#008bdb}.form-el-input:required{box-shadow:none}.form-label{margin-bottom:.5em}[class*=form-label][for]{cursor:pointer}.form-el-insider-wrap{display:table;width:100%}.form-el-insider-input{display:table-cell;width:100%}.form-el-insider{border-radius:2px;display:table-cell;padding:.43em .55em .5em 0;vertical-align:top}.form-legend,.form-legend-expand,.form-legend-light{display:block;margin:0}.form-legend,.form-legend-expand{font-size:1.25em;font-weight:600;margin-bottom:2.5em;padding-top:1.5em}.form-legend{border-top:1px solid #ccc;width:100%}.form-legend-light{font-size:1em;margin-bottom:1.5em}.form-legend-expand{cursor:pointer;transition:opacity .2s linear}.form-legend-expand:hover{opacity:.85}.form-legend-expand.expanded:after{content:'\e615'}.form-legend-expand:after{content:'\e616';font-family:Icons;font-size:1.15em;font-weight:400;margin-left:.5em;vertical-align:sub}.form-el-checkbox.disabled+.form-label,.form-el-checkbox.disabled+.form-label:before,.form-el-checkbox[disabled]+.form-label,.form-el-checkbox[disabled]+.form-label:before,.form-el-radio.disabled+.form-label,.form-el-radio.disabled+.form-label:before,.form-el-radio[disabled]+.form-label,.form-el-radio[disabled]+.form-label:before{cursor:default;opacity:.5;pointer-events:none}.form-el-checkbox:not(.disabled)+.form-label:hover:before,.form-el-checkbox:not([disabled])+.form-label:hover:before,.form-el-radio:not(.disabled)+.form-label:hover:before,.form-el-radio:not([disabled])+.form-label:hover:before{border-color:#514943}.form-el-checkbox+.form-label,.form-el-radio+.form-label{font-weight:400;padding-left:2em;padding-right:0;position:relative;text-align:left;transition:border-color .1s linear}.form-el-checkbox+.form-label:before,.form-el-radio+.form-label:before{border:1px solid;content:'';left:0;position:absolute;top:.1rem;transition:border-color .1s linear}.form-el-checkbox+.form-label:before{background-color:#fff;border-color:#adadad;border-radius:2px;font-size:1.2rem;height:1.6rem;line-height:1.2;width:1.6rem}.form-el-checkbox:checked+.form-label::before{content:'\e62d';font-family:Icons}.form-el-radio+.form-label:before{background-color:#fff;border:1px solid #adadad;border-radius:100%;height:1.8rem;width:1.8rem}.form-el-radio+.form-label:after{background:0 0;border:.5rem solid transparent;border-radius:100%;content:'';height:0;left:.4rem;position:absolute;top:.5rem;transition:background .3s linear;width:0}.form-el-radio:checked+.form-label{cursor:default}.form-el-radio:checked+.form-label:after{border-color:#514943}.form-select-label{border:1px solid #adadad;color:#303030;cursor:pointer;display:block;overflow:hidden;position:relative;z-index:0}.form-select-label:hover,.form-select-label:hover:after{border-color:#949494}.form-select-label:active,.form-select-label:active:after,.form-select-label:focus,.form-select-label:focus:after{border-color:#008bdb}.form-select-label:after{background:#e3e3e3;border-left:1px solid #adadad;bottom:0;content:'';position:absolute;right:0;top:0;width:2.36em;z-index:-2}.ie9 .form-select-label:after{display:none}.form-select-label:before{border-color:#303030 transparent transparent;border-style:solid;border-width:5px 4px 0;content:'';height:0;margin-right:-4px;margin-top:-2.5px;position:absolute;right:1.18em;top:50%;width:0;z-index:-1}.ie9 .form-select-label:before{display:none}.form-select-label .form-el-select{background:0 0;border:none;border-radius:0;content:'';display:block;margin:0;padding:.35em calc(2.36em + 10%) .5em .55em;width:110%}.ie9 .form-select-label .form-el-select{padding-right:.55em;width:100%}.form-select-label .form-el-select::-ms-expand{display:none}.form-el-select{background:#fff;border:1px solid #adadad;border-radius:2px;color:#303030;display:block;padding:.35em .55em}.multiselect-custom{border:1px solid #adadad;height:45.2rem;margin:0 0 1.5rem;overflow:auto;position:relative}.multiselect-custom ul{margin:0;padding:0;list-style:none;min-width:29rem}.multiselect-custom .item{padding:1rem 1.4rem}.multiselect-custom .selected{background-color:#e0f6fe}.multiselect-custom .form-label{margin-bottom:0}[class*=form-el-].invalid{border-color:#e22626}[class*=form-el-].invalid+.error-container{display:block}.error-container{background-color:#fffbbb;border:1px solid #ee7d7d;color:#514943;display:none;font-size:1.19rem;margin-top:.2rem;padding:.8rem 1rem .9rem}.check-result-message{margin-left:.5em;min-height:3.68rem;-ms-align-items:center;-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex}.check-result-text{margin-left:.5em}body:not([class]){min-width:0}.container{display:block;margin:0 auto 4rem;max-width:100rem;padding:0}.abs-action-delete,.action-close:before,.action-next:before,.action-previous:before,.admin-user .admin__action-dropdown:before,.admin__action-multiselect-dropdown:before,.admin__action-multiselect-search-label:before,.admin__control-checkbox+label:before,.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:before,.admin__control-table .action-delete:before,.admin__current-filters-list .action-remove:before,.admin__data-grid-action-bookmarks .action-delete:before,.admin__data-grid-action-bookmarks .action-edit:before,.admin__data-grid-action-bookmarks .action-submit:before,.admin__data-grid-action-bookmarks .admin__action-dropdown:before,.admin__data-grid-action-columns .admin__action-dropdown:before,.admin__data-grid-action-export .admin__action-dropdown:before,.admin__field-fallback-reset:before,.admin__menu .level-0>a:before,.admin__page-nav-item-message .admin__page-nav-item-message-icon,.admin__page-nav-title._collapsible:after,.data-grid-filters-action-wrap .action-default:before,.data-grid-row-changed:after,.data-grid-row-parent>td .data-grid-checkbox-cell-inner:before,.data-grid-search-control-wrap .action-submit:before,.extensions-information .list .extension-delete,.icon-failed:before,.icon-success:before,.notifications-action:before,.notifications-close:before,.page-actions .page-actions-buttons>button.action-back:before,.page-actions .page-actions-buttons>button.back:before,.page-actions>button.action-back:before,.page-actions>button.back:before,.page-title-jumbo-success:before,.search-global-label:before,.selectmenu .action-delete:before,.selectmenu .action-edit:before,.selectmenu .action-save:before,.setup-home-item:before,.sticky-header .data-grid-search-control-wrap .data-grid-search-label:before,.store-switcher .dropdown-menu .dropdown-toolbar a:before,.tooltip .help a:before,.tooltip .help span:before{-webkit-font-smoothing:antialiased;font-family:Icons;font-style:normal;font-weight:400;line-height:1;speak:none}.text-stretch{margin-bottom:1.5em}.page-title-jumbo{font-size:4rem;font-weight:300;letter-spacing:-.05em;margin-bottom:2.9rem}.page-title-jumbo-success:before{color:#79a22e;content:'\e62d';font-size:3.9rem;margin-left:-.3rem;margin-right:2.4rem}.list{margin-bottom:3rem}.list-dot .list-item{display:list-item;list-style-position:inside;margin-bottom:1.2rem}.list-title{color:#333;font-size:1.4rem;font-weight:700;letter-spacing:.025em;margin-bottom:1.2rem}.list-item-failed:before,.list-item-success:before,.list-item-warning:before{font-family:Icons;font-size:1.6rem;top:0}.list-item-success:before{content:'\e62d';font-size:1.6rem}.list-item-failed:before{content:'\e632';font-size:1.4rem;left:.1rem;top:.2rem}.list-item-warning:before{content:'\e623';font-size:1.3rem;left:.2rem}.form-wrap{margin-bottom:3.6rem;padding-top:2.1rem}.form-el-label-horizontal{display:inline-block;font-size:1.3rem;font-weight:600;letter-spacing:.025em;margin-bottom:.4rem;margin-left:.4rem}.app-updater{min-width:768px}body._has-modal{height:100%;overflow:hidden;width:100%}.modals-overlay{z-index:899}.modal-popup,.modal-slide{bottom:0;min-width:0;position:fixed;right:0;top:0;visibility:hidden}.modal-popup._show,.modal-slide._show{visibility:visible}.modal-popup._show .modal-inner-wrap,.modal-slide._show .modal-inner-wrap{-ms-transform:translate(0,0);transform:translate(0,0)}.modal-popup .modal-inner-wrap,.modal-slide .modal-inner-wrap{background-color:#fff;box-shadow:0 0 12px 2px rgba(0,0,0,.35);opacity:1;pointer-events:auto}.modal-slide{left:14.8rem;z-index:900}.modal-slide._show .modal-inner-wrap{-ms-transform:translateX(0);transform:translateX(0)}.modal-slide .modal-inner-wrap{height:100%;overflow-y:auto;position:static;-ms-transform:translateX(100%);transform:translateX(100%);transition-duration:.3s;transition-property:transform,visibility;transition-timing-function:ease-in-out;width:auto}.modal-slide._inner-scroll .modal-inner-wrap{overflow-y:visible;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.modal-slide._inner-scroll .modal-footer,.modal-slide._inner-scroll .modal-header{-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0}.modal-slide._inner-scroll .modal-content{overflow-y:auto}.modal-slide._inner-scroll .modal-footer{margin-top:auto}.modal-slide .modal-content,.modal-slide .modal-footer,.modal-slide .modal-header{padding:0 2.6rem 2.6rem}.modal-slide .modal-header{padding-bottom:2.1rem;padding-top:2.1rem}.modal-popup{z-index:900;left:0;overflow-y:auto}.modal-popup._show .modal-inner-wrap{-ms-transform:translateY(0);transform:translateY(0)}.modal-popup .modal-inner-wrap{margin:5rem auto;width:75%;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;box-sizing:border-box;height:auto;left:0;position:absolute;right:0;-ms-transform:translateY(-200%);transform:translateY(-200%);transition-duration:.2s;transition-property:transform,visibility;transition-timing-function:ease}.modal-popup._inner-scroll{overflow-y:visible}.ie10 .modal-popup._inner-scroll,.ie9 .modal-popup._inner-scroll{overflow-y:auto}.modal-popup._inner-scroll .modal-inner-wrap{max-height:90%}.ie10 .modal-popup._inner-scroll .modal-inner-wrap,.ie9 .modal-popup._inner-scroll .modal-inner-wrap{max-height:none}.modal-popup._inner-scroll .modal-content{overflow-y:auto}.modal-popup .modal-content,.modal-popup .modal-footer,.modal-popup .modal-header{padding-left:3rem;padding-right:3rem}.modal-popup .modal-footer,.modal-popup .modal-header{-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0}.modal-popup .modal-header{padding-bottom:1.2rem;padding-top:3rem}.modal-popup .modal-footer{margin-top:auto;padding-bottom:3rem}.modal-popup .modal-footer-actions{text-align:right}.admin__action-dropdown-wrap{display:inline-block;position:relative}.admin__action-dropdown-wrap .admin__action-dropdown-text:after{left:-6px;right:0}.admin__action-dropdown-wrap .admin__action-dropdown-menu{left:auto;right:0}.admin__action-dropdown-wrap._active .admin__action-dropdown,.admin__action-dropdown-wrap.active .admin__action-dropdown{border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.admin__action-dropdown-wrap._active .admin__action-dropdown-text:after,.admin__action-dropdown-wrap.active .admin__action-dropdown-text:after{background-color:#fff;content:'';height:6px;position:absolute;top:100%}.admin__action-dropdown-wrap._active .admin__action-dropdown-menu,.admin__action-dropdown-wrap.active .admin__action-dropdown-menu{display:block}.admin__action-dropdown-wrap._disabled .admin__action-dropdown{cursor:default}.admin__action-dropdown-wrap._disabled:hover .admin__action-dropdown{color:#333}.admin__action-dropdown{background-color:#fff;border:1px solid transparent;border-bottom:none;border-radius:0;box-shadow:none;color:#333;display:inline-block;font-size:1.3rem;font-weight:400;letter-spacing:-.025em;padding:.7rem 3.3rem .8rem 1.5rem;position:relative;vertical-align:baseline;z-index:2}.admin__action-dropdown._active:after,.admin__action-dropdown.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin__action-dropdown:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;top:50%;transition:all .2s linear;width:0}._active .admin__action-dropdown:after,.active .admin__action-dropdown:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin__action-dropdown:hover:after{border-color:#000 transparent transparent}.admin__action-dropdown:focus,.admin__action-dropdown:hover{background-color:#fff;color:#000;text-decoration:none}.admin__action-dropdown:after{right:1.5rem}.admin__action-dropdown:before{margin-right:1rem}.admin__action-dropdown-menu{background-color:#fff;border:1px solid #007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5);display:none;line-height:1.36;margin-top:-1px;min-width:120%;padding:.5rem 1rem;position:absolute;top:100%;transition:all .15s ease;z-index:1}.admin__action-dropdown-menu>li{display:block}.admin__action-dropdown-menu>li>a{color:#333;display:block;text-decoration:none;padding:.6rem .5rem}.selectmenu{display:inline-block;position:relative;text-align:left;z-index:1}.selectmenu._active{border-color:#007bdb;z-index:500}.selectmenu .action-delete,.selectmenu .action-edit,.selectmenu .action-save{background-color:transparent;border-color:transparent;box-shadow:none;padding:0 1rem}.selectmenu .action-delete:hover,.selectmenu .action-edit:hover,.selectmenu .action-save:hover{background-color:transparent;border-color:transparent;box-shadow:none}.selectmenu .action-delete:before,.selectmenu .action-edit:before,.selectmenu .action-save:before{content:'\e630'}.selectmenu .action-delete,.selectmenu .action-edit{border:0 solid #fff;border-left-width:1px;bottom:0;position:absolute;right:0;top:0;z-index:1}.selectmenu .action-delete:hover,.selectmenu .action-edit:hover{border:0 solid #fff;border-left-width:1px}.selectmenu .action-save:before{content:'\e625'}.selectmenu .action-edit:before{content:'\e631'}.selectmenu-value{display:inline-block}.selectmenu-value input[type=text]{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;border:0;display:inline;margin:0;width:6rem}body._keyfocus .selectmenu-value input[type=text]:focus{box-shadow:none}.selectmenu-toggle{padding-right:3rem;background:0 0;border-width:0;bottom:0;float:right;position:absolute;right:0;top:0;width:0}.selectmenu-toggle._active:after,.selectmenu-toggle.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.selectmenu-toggle:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.1rem;top:50%;transition:all .2s linear;width:0}._active .selectmenu-toggle:after,.active .selectmenu-toggle:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.selectmenu-toggle:hover:after{border-color:#000 transparent transparent}.selectmenu-toggle:active,.selectmenu-toggle:focus,.selectmenu-toggle:hover{background:0 0}.selectmenu._active .selectmenu-toggle:before{border-color:#007bdb}body._keyfocus .selectmenu-toggle:focus{box-shadow:none}.selectmenu-toggle:before{background:#e3e3e3;border-left:1px solid #adadad;bottom:0;content:'';display:block;position:absolute;right:0;top:0;width:3.2rem}.selectmenu-items{background:#fff;border:1px solid #007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5);display:none;float:left;left:-1px;margin-top:3px;max-width:20rem;min-width:calc(100% + 2px);position:absolute;top:100%}.selectmenu-items._active{display:block}.selectmenu-items ul{float:left;list-style-type:none;margin:0;min-width:100%;padding:0}.selectmenu-items li{-webkit-flex-direction:row;display:flex;-ms-flex-direction:row;flex-direction:row;transition:background .2s linear}.selectmenu-items li:hover{background:#e3e3e3}.selectmenu-items li:last-child .selectmenu-item-action,.selectmenu-items li:last-child .selectmenu-item-action:visited{color:#008bdb;text-decoration:none}.selectmenu-items li:last-child .selectmenu-item-action:hover{color:#0fa7ff;text-decoration:underline}.selectmenu-items li:last-child .selectmenu-item-action:active{color:#ff5501;text-decoration:underline}.selectmenu-item{position:relative;width:100%;z-index:1}li._edit>.selectmenu-item{display:none}.selectmenu-item-edit{display:none;padding:.3rem 4rem .3rem .4rem;position:relative;white-space:nowrap;z-index:1}li:last-child .selectmenu-item-edit{padding-right:.4rem}.selectmenu-item-edit .admin__control-text{margin:0;width:5.4rem}li._edit .selectmenu-item-edit{display:block}.selectmenu-item-action{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;background:0 0;border:0;color:#333;display:block;font-size:1.4rem;font-weight:400;min-width:100%;padding:1rem 6rem 1rem 1.5rem;text-align:left;transition:background .2s linear;width:5rem}.selectmenu-item-action:focus,.selectmenu-item-action:hover{background:#e3e3e3}.abs-actions-split-xl .action-default,.page-actions .actions-split .action-default{margin-right:4rem}.abs-actions-split-xl .action-toggle,.page-actions .actions-split .action-toggle{padding-right:4rem}.abs-actions-split-xl .action-toggle:after,.page-actions .actions-split .action-toggle:after{border-width:.9rem .6rem 0;margin-top:-.3rem;right:1.4rem}.actions-split{position:relative;z-index:400}.actions-split._active,.actions-split.active,.actions-split:hover{box-shadow:0 0 0 1px #007bdb}.actions-split._active .action-toggle.action-primary,.actions-split._active .action-toggle.primary,.actions-split.active .action-toggle.action-primary,.actions-split.active .action-toggle.primary{background-color:#ba4000;border-color:#ba4000}.actions-split._active .dropdown-menu,.actions-split.active .dropdown-menu{opacity:1;visibility:visible;display:block}.actions-split .action-default,.actions-split .action-toggle{float:left;margin:0}.actions-split .action-default._active,.actions-split .action-default.active,.actions-split .action-default:hover,.actions-split .action-toggle._active,.actions-split .action-toggle.active,.actions-split .action-toggle:hover{box-shadow:none}.actions-split .action-default{margin-right:3.2rem;min-width:9.3rem}.actions-split .action-toggle{padding-right:3.2rem;border-left-color:rgba(0,0,0,.2);bottom:0;padding-left:0;position:absolute;right:0;top:0}.actions-split .action-toggle._active:after,.actions-split .action-toggle.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.actions-split .action-toggle:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.2rem;top:50%;transition:all .2s linear;width:0}._active .actions-split .action-toggle:after,.active .actions-split .action-toggle:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.actions-split .action-toggle:hover:after{border-color:#000 transparent transparent}.actions-split .action-toggle.action-primary:after,.actions-split .action-toggle.action-secondary:after,.actions-split .action-toggle.primary:after,.actions-split .action-toggle.secondary:after{border-color:#fff transparent transparent}.actions-split .action-toggle>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.action-select-wrap{display:inline-block;position:relative}.action-select-wrap .action-select{padding-right:3.2rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background-color:#fff;font-weight:400;text-align:left}.action-select-wrap .action-select._active:after,.action-select-wrap .action-select.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-select-wrap .action-select:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.2rem;top:50%;transition:all .2s linear;width:0}._active .action-select-wrap .action-select:after,.active .action-select-wrap .action-select:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-select-wrap .action-select:hover:after{border-color:#000 transparent transparent}.action-select-wrap .action-select:hover,.action-select-wrap .action-select:hover:before{border-color:#878787}.action-select-wrap .action-select:before{background-color:#e3e3e3;border:1px solid #adadad;bottom:0;content:'';position:absolute;right:0;top:0;width:3.2rem}.action-select-wrap .action-select._active{border-color:#007bdb}.action-select-wrap .action-select._active:before{border-color:#007bdb #007bdb #007bdb #adadad}.action-select-wrap .action-select[disabled]{color:#333}.action-select-wrap .action-select[disabled]:after{border-color:#333 transparent transparent}.action-select-wrap._active{z-index:500}.action-select-wrap._active .action-select,.action-select-wrap._active .action-select:before{border-color:#007bdb}.action-select-wrap._active .action-select:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-select-wrap .abs-action-menu .action-submenu,.action-select-wrap .abs-action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu,.action-select-wrap .action-menu .action-submenu,.action-select-wrap .actions-split .action-menu .action-submenu,.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu,.action-select-wrap .actions-split .dropdown-menu .action-submenu,.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{max-height:45rem;overflow-y:auto}.action-select-wrap .abs-action-menu .action-submenu ._disabled:hover,.action-select-wrap .abs-action-menu .action-submenu .action-submenu ._disabled:hover,.action-select-wrap .action-menu ._disabled:hover,.action-select-wrap .action-menu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .action-menu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .dropdown-menu .action-submenu ._disabled:hover,.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu ._disabled:hover{background:#fff}.action-select-wrap .abs-action-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .abs-action-menu .action-submenu .action-submenu ._disabled .action-menu-item,.action-select-wrap .action-menu ._disabled .action-menu-item,.action-select-wrap .action-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .action-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .dropdown-menu .action-submenu ._disabled .action-menu-item,.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu ._disabled .action-menu-item{cursor:default;opacity:.5}.action-select-wrap .action-menu-items{left:0;position:absolute;right:0;top:100%}.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu,.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.action-menu,.action-select-wrap .action-menu-items>.action-menu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu .action-submenu{min-width:100%;position:static}.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.abs-action-menu .action-submenu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.action-menu .action-submenu,.action-select-wrap .action-menu-items>.action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .action-menu .action-submenu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu .action-submenu,.action-select-wrap .action-menu-items>.actions-split .dropdown-menu .action-submenu .action-submenu .action-submenu{position:absolute}.action-multicheck-wrap{display:inline-block;height:1.6rem;padding-top:1px;position:relative;width:3.1rem;z-index:200}.action-multicheck-wrap:hover .action-multicheck-toggle,.action-multicheck-wrap:hover .admin__control-checkbox+label:before{border-color:#878787}.action-multicheck-wrap._active .action-multicheck-toggle,.action-multicheck-wrap._active .admin__control-checkbox+label:before{border-color:#007bdb}.action-multicheck-wrap._active .abs-action-menu .action-submenu,.action-multicheck-wrap._active .abs-action-menu .action-submenu .action-submenu,.action-multicheck-wrap._active .action-menu,.action-multicheck-wrap._active .action-menu .action-submenu,.action-multicheck-wrap._active .actions-split .action-menu .action-submenu,.action-multicheck-wrap._active .actions-split .action-menu .action-submenu .action-submenu,.action-multicheck-wrap._active .actions-split .dropdown-menu .action-submenu,.action-multicheck-wrap._active .actions-split .dropdown-menu .action-submenu .action-submenu{opacity:1;visibility:visible;display:block}.action-multicheck-wrap._disabled .admin__control-checkbox+label:before{background-color:#fff}.action-multicheck-wrap._disabled .action-multicheck-toggle,.action-multicheck-wrap._disabled .admin__control-checkbox+label:before{border-color:#adadad;opacity:1}.action-multicheck-wrap .action-multicheck-toggle,.action-multicheck-wrap .admin__control-checkbox,.action-multicheck-wrap .admin__control-checkbox+label{float:left}.action-multicheck-wrap .action-multicheck-toggle{border-radius:0 1px 1px 0;height:1.6rem;margin-left:-1px;padding:0;position:relative;transition:border-color .1s linear;width:1.6rem}.action-multicheck-wrap .action-multicheck-toggle._active:after,.action-multicheck-wrap .action-multicheck-toggle.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-multicheck-wrap .action-multicheck-toggle:after{border-color:#000 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;top:50%;transition:all .2s linear;width:0}._active .action-multicheck-wrap .action-multicheck-toggle:after,.active .action-multicheck-wrap .action-multicheck-toggle:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.action-multicheck-wrap .action-multicheck-toggle:hover:after{border-color:#000 transparent transparent}.action-multicheck-wrap .action-multicheck-toggle:focus{border-color:#007bdb}.action-multicheck-wrap .action-multicheck-toggle:after{right:.3rem}.action-multicheck-wrap .abs-action-menu .action-submenu,.action-multicheck-wrap .abs-action-menu .action-submenu .action-submenu,.action-multicheck-wrap .action-menu,.action-multicheck-wrap .action-menu .action-submenu,.action-multicheck-wrap .actions-split .action-menu .action-submenu,.action-multicheck-wrap .actions-split .action-menu .action-submenu .action-submenu,.action-multicheck-wrap .actions-split .dropdown-menu .action-submenu,.action-multicheck-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{left:-1.1rem;margin-top:1px;right:auto;text-align:left}.action-multicheck-wrap .action-menu-item{white-space:nowrap}.admin__action-multiselect-wrap{display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.admin__action-multiselect-wrap.action-select-wrap:focus{box-shadow:none}.admin__action-multiselect-wrap.action-select-wrap .abs-action-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .abs-action-menu .action-submenu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .action-menu,.admin__action-multiselect-wrap.action-select-wrap .action-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .action-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .action-menu .action-submenu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .dropdown-menu .action-submenu,.admin__action-multiselect-wrap.action-select-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{max-height:none;overflow-y:inherit}.admin__action-multiselect-wrap .action-menu-item{transition:background-color .1s linear}.admin__action-multiselect-wrap .action-menu-item._selected{background-color:#e0f6fe}.admin__action-multiselect-wrap .action-menu-item._hover{background-color:#e3e3e3}.admin__action-multiselect-wrap .action-menu-item._unclickable{cursor:default}.admin__action-multiselect-wrap .admin__action-multiselect{border:1px solid #adadad;cursor:pointer;display:block;min-height:3.2rem;padding-right:3.6rem;white-space:normal}.admin__action-multiselect-wrap .admin__action-multiselect:after{bottom:1.25rem;top:auto}.admin__action-multiselect-wrap .admin__action-multiselect:before{height:3.3rem;top:auto}.admin__control-table-wrapper .admin__action-multiselect-wrap{position:static}.admin__control-table-wrapper .admin__action-multiselect-wrap .admin__action-multiselect{position:relative}.admin__control-table-wrapper .admin__action-multiselect-wrap .admin__action-multiselect:before{right:-1px;top:-1px}.admin__control-table-wrapper .admin__action-multiselect-wrap .abs-action-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .abs-action-menu .action-submenu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .action-menu,.admin__control-table-wrapper .admin__action-multiselect-wrap .action-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .action-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .action-menu .action-submenu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .dropdown-menu .action-submenu,.admin__control-table-wrapper .admin__action-multiselect-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{left:auto;min-width:34rem;right:auto;top:auto;z-index:1}.admin__action-multiselect-wrap .admin__action-multiselect-item-path{color:#a79d95;font-size:1.2rem;font-weight:400;padding-left:1rem}.admin__action-multiselect-actions-wrap{border-top:1px solid #e3e3e3;margin:0 1rem;padding:1rem 0;text-align:center}.admin__action-multiselect-actions-wrap .action-default{font-size:1.3rem;min-width:13rem}.admin__action-multiselect-text{padding:.6rem 1rem}.abs-action-menu .action-submenu,.abs-action-menu .action-submenu .action-submenu,.action-menu,.action-menu .action-submenu,.actions-split .action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu,.actions-split .dropdown-menu .action-submenu .action-submenu{text-align:left}.admin__action-multiselect-label{cursor:pointer;position:relative;z-index:1}.admin__action-multiselect-label:before{margin-right:.5rem}._unclickable .admin__action-multiselect-label{cursor:default;font-weight:700}.admin__action-multiselect-search-wrap{border-bottom:1px solid #e3e3e3;margin:0 1rem;padding:1rem 0;position:relative}.admin__action-multiselect-search{padding-right:3rem;width:100%}.admin__action-multiselect-search-label{display:block;font-size:1.5rem;height:1em;overflow:hidden;position:absolute;right:2.2rem;top:1.7rem;width:1em}.admin__action-multiselect-search-label:before{content:'\e60c'}.admin__action-multiselect-search-count{color:#a79d95;margin-top:1rem}.admin__action-multiselect-menu-inner{margin-bottom:0;max-height:46rem;overflow-y:auto}.admin__action-multiselect-menu-inner .admin__action-multiselect-menu-inner{list-style:none;max-height:none;overflow:hidden;padding-left:2.2rem}.admin__action-multiselect-menu-inner ._hidden{display:none}.admin__action-multiselect-crumb{background-color:#f5f5f5;border:1px solid #a79d95;border-radius:1px;display:inline-block;font-size:1.2rem;margin:.3rem -4px .3rem .3rem;padding:.3rem 2.4rem .4rem 1rem;position:relative;transition:border-color .1s linear}.admin__action-multiselect-crumb:hover{border-color:#908379}.admin__action-multiselect-crumb .action-close{bottom:0;font-size:.5em;position:absolute;right:0;top:0;width:2rem}.admin__action-multiselect-crumb .action-close:hover{color:#000}.admin__action-multiselect-crumb .action-close:active,.admin__action-multiselect-crumb .action-close:focus{background-color:transparent}.admin__action-multiselect-crumb .action-close:active{-ms-transform:scale(0.9);transform:scale(0.9)}.admin__action-multiselect-tree .abs-action-menu .action-submenu,.admin__action-multiselect-tree .abs-action-menu .action-submenu .action-submenu,.admin__action-multiselect-tree .action-menu,.admin__action-multiselect-tree .action-menu .action-submenu,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu .action-submenu,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu .action-submenu{min-width:34.7rem}.admin__action-multiselect-tree .abs-action-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .abs-action-menu .action-submenu .action-submenu .action-menu-item,.admin__action-multiselect-tree .action-menu .action-menu-item,.admin__action-multiselect-tree .action-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .action-menu .action-submenu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu .action-menu-item,.admin__action-multiselect-tree .actions-split .dropdown-menu .action-submenu .action-submenu .action-menu-item{margin-top:.1rem}.admin__action-multiselect-tree .action-menu-item{margin-left:4.2rem;position:relative}.admin__action-multiselect-tree .action-menu-item._expended:before{border-left:1px dashed #a79d95;bottom:0;content:'';left:-1rem;position:absolute;top:1rem;width:1px}.admin__action-multiselect-tree .action-menu-item._expended .admin__action-multiselect-dropdown:before{content:'\e615'}.admin__action-multiselect-tree .action-menu-item._with-checkbox .admin__action-multiselect-label{padding-left:2.6rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner{position:relative}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner .admin__action-multiselect-menu-inner{padding-left:3.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner .admin__action-multiselect-menu-inner:before{left:4.3rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item{position:relative}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:last-child:before{height:2.1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:after,.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:before{content:'';left:0;position:absolute}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:after{border-top:1px dashed #a79d95;height:1px;top:2.1rem;width:5.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item:before{border-left:1px dashed #a79d95;height:100%;top:0;width:1px}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._parent:after{width:4.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root{margin-left:-1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:after{left:3.2rem;width:2.2rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:before{left:3.2rem;top:1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root._parent:after{display:none}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:first-child:before{top:2.1rem}.admin__action-multiselect-tree .admin__action-multiselect-menu-inner-item._root:last-child:before{height:1rem}.admin__action-multiselect-tree .admin__action-multiselect-label{line-height:2.2rem;vertical-align:middle;word-break:break-all}.admin__action-multiselect-tree .admin__action-multiselect-label:before{left:0;position:absolute;top:.4rem}.admin__action-multiselect-dropdown{border-radius:50%;height:2.2rem;left:-2.2rem;position:absolute;top:1rem;width:2.2rem;z-index:1}.admin__action-multiselect-dropdown:before{background:#fff;color:#a79d95;content:'\e616';font-size:2.2rem}.admin__actions-switch{display:inline-block;position:relative;vertical-align:middle}.admin__field-control .admin__actions-switch{line-height:3.2rem}.admin__actions-switch+.admin__field-service{min-width:34rem}._disabled .admin__actions-switch-checkbox+.admin__actions-switch-label,.admin__actions-switch-checkbox.disabled+.admin__actions-switch-label{cursor:not-allowed;opacity:.5;pointer-events:none}.admin__actions-switch-checkbox:checked+.admin__actions-switch-label:before{left:15px}.admin__actions-switch-checkbox:checked+.admin__actions-switch-label:after{background:#79a22e}.admin__actions-switch-checkbox:checked+.admin__actions-switch-label .admin__actions-switch-text:before{content:attr(data-text-on)}.admin__actions-switch-checkbox:focus+.admin__actions-switch-label:after,.admin__actions-switch-checkbox:focus+.admin__actions-switch-label:before{border-color:#007bdb}._error .admin__actions-switch-checkbox+.admin__actions-switch-label:after,._error .admin__actions-switch-checkbox+.admin__actions-switch-label:before{border-color:#e22626}.admin__actions-switch-label{cursor:pointer;display:inline-block;height:22px;line-height:22px;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle}.admin__actions-switch-label:after,.admin__actions-switch-label:before{left:0;position:absolute;right:auto;top:0}.admin__actions-switch-label:before{background:#fff;border:1px solid #aaa6a0;border-radius:100%;content:'';display:block;height:22px;transition:left .2s ease-in 0s;width:22px;z-index:1}.admin__actions-switch-label:after{background:#e3e3e3;border:1px solid #aaa6a0;border-radius:12px;content:'';display:block;height:22px;transition:background .2s ease-in 0s;vertical-align:middle;width:37px;z-index:0}.admin__actions-switch-text:before{content:attr(data-text-off);padding-left:47px;white-space:nowrap}.abs-action-delete,.abs-action-reset,.action-close,.admin__field-fallback-reset,.extensions-information .list .extension-delete,.notifications-close,.search-global-field._active .search-global-action{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;padding:0}.abs-action-delete:hover,.abs-action-reset:hover,.action-close:hover,.admin__field-fallback-reset:hover,.extensions-information .list .extension-delete:hover,.notifications-close:hover,.search-global-field._active .search-global-action:hover{background-color:transparent;border:none;box-shadow:none}.abs-action-default,.abs-action-pattern,.abs-action-primary,.abs-action-quaternary,.abs-action-secondary,.abs-action-tertiary,.action-default,.action-primary,.action-quaternary,.action-secondary,.action-tertiary,.modal-popup .modal-footer .action-primary,.modal-popup .modal-footer .action-secondary,.page-actions .page-actions-buttons>button,.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.action-secondary,.page-actions .page-actions-buttons>button.primary,.page-actions>button,.page-actions>button.action-primary,.page-actions>button.action-secondary,.page-actions>button.primary,button,button.primary,button.secondary,button.tertiary{border:1px solid;border-radius:0;display:inline-block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:600;line-height:1.36;padding:.6rem 1em;text-align:center;vertical-align:baseline}.abs-action-default.disabled,.abs-action-default[disabled],.abs-action-pattern.disabled,.abs-action-pattern[disabled],.abs-action-primary.disabled,.abs-action-primary[disabled],.abs-action-quaternary.disabled,.abs-action-quaternary[disabled],.abs-action-secondary.disabled,.abs-action-secondary[disabled],.abs-action-tertiary.disabled,.abs-action-tertiary[disabled],.action-default.disabled,.action-default[disabled],.action-primary.disabled,.action-primary[disabled],.action-quaternary.disabled,.action-quaternary[disabled],.action-secondary.disabled,.action-secondary[disabled],.action-tertiary.disabled,.action-tertiary[disabled],.modal-popup .modal-footer .action-primary.disabled,.modal-popup .modal-footer .action-primary[disabled],.modal-popup .modal-footer .action-secondary.disabled,.modal-popup .modal-footer .action-secondary[disabled],.page-actions .page-actions-buttons>button.action-primary.disabled,.page-actions .page-actions-buttons>button.action-primary[disabled],.page-actions .page-actions-buttons>button.action-secondary.disabled,.page-actions .page-actions-buttons>button.action-secondary[disabled],.page-actions .page-actions-buttons>button.disabled,.page-actions .page-actions-buttons>button.primary.disabled,.page-actions .page-actions-buttons>button.primary[disabled],.page-actions .page-actions-buttons>button[disabled],.page-actions>button.action-primary.disabled,.page-actions>button.action-primary[disabled],.page-actions>button.action-secondary.disabled,.page-actions>button.action-secondary[disabled],.page-actions>button.disabled,.page-actions>button.primary.disabled,.page-actions>button.primary[disabled],.page-actions>button[disabled],button.disabled,button.primary.disabled,button.primary[disabled],button.secondary.disabled,button.secondary[disabled],button.tertiary.disabled,button.tertiary[disabled],button[disabled]{cursor:default;opacity:.5;pointer-events:none}.abs-action-l,.modal-popup .modal-footer .action-primary,.modal-popup .modal-footer .action-secondary,.page-actions .page-actions-buttons>button,.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.action-secondary,.page-actions .page-actions-buttons>button.primary,.page-actions button,.page-actions>button.action-primary,.page-actions>button.action-secondary,.page-actions>button.primary{font-size:1.6rem;letter-spacing:.025em;padding-bottom:.6875em;padding-top:.6875em}.abs-action-delete,.extensions-information .list .extension-delete{display:inline-block;font-size:1.6rem;margin-left:1.2rem;padding-top:.7rem;text-decoration:none;vertical-align:middle}.abs-action-delete:after,.extensions-information .list .extension-delete:after{color:#666;content:'\e630'}.abs-action-delete:hover:after,.extensions-information .list .extension-delete:hover:after{color:#35302c}.abs-action-button-as-link,.action-advanced,.data-grid .action-delete{line-height:1.36;padding:0;color:#008bdb;text-decoration:none;background:0 0;border:0;display:inline;font-weight:400;border-radius:0}.abs-action-button-as-link:visited,.action-advanced:visited,.data-grid .action-delete:visited{color:#008bdb;text-decoration:none}.abs-action-button-as-link:hover,.action-advanced:hover,.data-grid .action-delete:hover{text-decoration:underline}.abs-action-button-as-link:active,.action-advanced:active,.data-grid .action-delete:active{color:#ff5501;text-decoration:underline}.abs-action-button-as-link:hover,.action-advanced:hover,.data-grid .action-delete:hover{color:#0fa7ff}.abs-action-button-as-link:active,.abs-action-button-as-link:focus,.abs-action-button-as-link:hover,.action-advanced:active,.action-advanced:focus,.action-advanced:hover,.data-grid .action-delete:active,.data-grid .action-delete:focus,.data-grid .action-delete:hover{background:0 0;border:0}.abs-action-button-as-link.disabled,.abs-action-button-as-link[disabled],.action-advanced.disabled,.action-advanced[disabled],.data-grid .action-delete.disabled,.data-grid .action-delete[disabled],fieldset[disabled] .abs-action-button-as-link,fieldset[disabled] .action-advanced,fieldset[disabled] .data-grid .action-delete{color:#008bdb;opacity:.5;cursor:default;pointer-events:none;text-decoration:underline}.abs-action-button-as-link:active,.abs-action-button-as-link:not(:focus),.action-advanced:active,.action-advanced:not(:focus),.data-grid .action-delete:active,.data-grid .action-delete:not(:focus){box-shadow:none}.abs-action-button-as-link:focus,.action-advanced:focus,.data-grid .action-delete:focus{color:#0fa7ff}.abs-action-default,button{background:#e3e3e3;border-color:#adadad;color:#514943}.abs-action-default:active,.abs-action-default:focus,.abs-action-default:hover,button:active,button:focus,button:hover{background-color:#dbdbdb;color:#514943;text-decoration:none}.abs-action-primary,.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.primary,.page-actions>button.action-primary,.page-actions>button.primary,button.primary{background-color:#eb5202;border-color:#eb5202;color:#fff;text-shadow:1px 1px 0 rgba(0,0,0,.25)}.abs-action-primary:active,.abs-action-primary:focus,.abs-action-primary:hover,.page-actions .page-actions-buttons>button.action-primary:active,.page-actions .page-actions-buttons>button.action-primary:focus,.page-actions .page-actions-buttons>button.action-primary:hover,.page-actions .page-actions-buttons>button.primary:active,.page-actions .page-actions-buttons>button.primary:focus,.page-actions .page-actions-buttons>button.primary:hover,.page-actions>button.action-primary:active,.page-actions>button.action-primary:focus,.page-actions>button.action-primary:hover,.page-actions>button.primary:active,.page-actions>button.primary:focus,.page-actions>button.primary:hover,button.primary:active,button.primary:focus,button.primary:hover{background-color:#ba4000;border-color:#b84002;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.abs-action-primary.disabled,.abs-action-primary[disabled],.page-actions .page-actions-buttons>button.action-primary.disabled,.page-actions .page-actions-buttons>button.action-primary[disabled],.page-actions .page-actions-buttons>button.primary.disabled,.page-actions .page-actions-buttons>button.primary[disabled],.page-actions>button.action-primary.disabled,.page-actions>button.action-primary[disabled],.page-actions>button.primary.disabled,.page-actions>button.primary[disabled],button.primary.disabled,button.primary[disabled]{cursor:default;opacity:.5;pointer-events:none}.abs-action-secondary,.modal-popup .modal-footer .action-primary,.page-actions .page-actions-buttons>button.action-secondary,.page-actions>button.action-secondary,button.secondary{background-color:#514943;border-color:#514943;color:#fff;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.abs-action-secondary:active,.abs-action-secondary:focus,.abs-action-secondary:hover,.modal-popup .modal-footer .action-primary:active,.modal-popup .modal-footer .action-primary:focus,.modal-popup .modal-footer .action-primary:hover,.page-actions .page-actions-buttons>button.action-secondary:active,.page-actions .page-actions-buttons>button.action-secondary:focus,.page-actions .page-actions-buttons>button.action-secondary:hover,.page-actions>button.action-secondary:active,.page-actions>button.action-secondary:focus,.page-actions>button.action-secondary:hover,button.secondary:active,button.secondary:focus,button.secondary:hover{background-color:#35302c;border-color:#35302c;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.abs-action-secondary:active,.modal-popup .modal-footer .action-primary:active,.page-actions .page-actions-buttons>button.action-secondary:active,.page-actions>button.action-secondary:active,button.secondary:active{background-color:#35302c}.abs-action-tertiary,.modal-popup .modal-footer .action-secondary,button.tertiary{background-color:transparent;border-color:transparent;text-shadow:none;color:#008bdb}.abs-action-tertiary:active,.abs-action-tertiary:focus,.abs-action-tertiary:hover,.modal-popup .modal-footer .action-secondary:active,.modal-popup .modal-footer .action-secondary:focus,.modal-popup .modal-footer .action-secondary:hover,button.tertiary:active,button.tertiary:focus,button.tertiary:hover{background-color:transparent;border-color:transparent;box-shadow:none;color:#0fa7ff;text-decoration:underline}.abs-action-quaternary,.page-actions .page-actions-buttons>button,.page-actions>button{background-color:transparent;border-color:transparent;text-shadow:none;color:#333}.abs-action-quaternary:active,.abs-action-quaternary:focus,.abs-action-quaternary:hover,.page-actions .page-actions-buttons>button:active,.page-actions .page-actions-buttons>button:focus,.page-actions .page-actions-buttons>button:hover,.page-actions>button:active,.page-actions>button:focus,.page-actions>button:hover{background-color:transparent;border-color:transparent;box-shadow:none;color:#1a1a1a}.abs-action-menu,.actions-split .abs-action-menu .action-submenu,.actions-split .abs-action-menu .action-submenu .action-submenu,.actions-split .action-menu,.actions-split .action-menu .action-submenu,.actions-split .actions-split .dropdown-menu .action-submenu,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu,.actions-split .dropdown-menu{text-align:left;background-color:#fff;border:1px solid #007bdb;border-radius:1px;box-shadow:1px 1px 5px rgba(0,0,0,.5);color:#333;display:none;font-weight:400;left:0;list-style:none;margin:2px 0 0;min-width:0;padding:0;position:absolute;right:0;top:100%}.abs-action-menu._active,.actions-split .abs-action-menu .action-submenu .action-submenu._active,.actions-split .abs-action-menu .action-submenu._active,.actions-split .action-menu .action-submenu._active,.actions-split .action-menu._active,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu._active,.actions-split .actions-split .dropdown-menu .action-submenu._active,.actions-split .dropdown-menu._active{display:block}.abs-action-menu>li,.actions-split .abs-action-menu .action-submenu .action-submenu>li,.actions-split .abs-action-menu .action-submenu>li,.actions-split .action-menu .action-submenu>li,.actions-split .action-menu>li,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li,.actions-split .actions-split .dropdown-menu .action-submenu>li,.actions-split .dropdown-menu>li{border:none;display:block;padding:0;transition:background-color .1s linear}.abs-action-menu>li>a:hover,.actions-split .abs-action-menu .action-submenu .action-submenu>li>a:hover,.actions-split .abs-action-menu .action-submenu>li>a:hover,.actions-split .action-menu .action-submenu>li>a:hover,.actions-split .action-menu>li>a:hover,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li>a:hover,.actions-split .actions-split .dropdown-menu .action-submenu>li>a:hover,.actions-split .dropdown-menu>li>a:hover{text-decoration:none}.abs-action-menu>li._visible,.abs-action-menu>li:hover,.actions-split .abs-action-menu .action-submenu .action-submenu>li._visible,.actions-split .abs-action-menu .action-submenu .action-submenu>li:hover,.actions-split .abs-action-menu .action-submenu>li._visible,.actions-split .abs-action-menu .action-submenu>li:hover,.actions-split .action-menu .action-submenu>li._visible,.actions-split .action-menu .action-submenu>li:hover,.actions-split .action-menu>li._visible,.actions-split .action-menu>li:hover,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li._visible,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li:hover,.actions-split .actions-split .dropdown-menu .action-submenu>li._visible,.actions-split .actions-split .dropdown-menu .action-submenu>li:hover,.actions-split .dropdown-menu>li._visible,.actions-split .dropdown-menu>li:hover{background-color:#e3e3e3}.abs-action-menu>li:active,.actions-split .abs-action-menu .action-submenu .action-submenu>li:active,.actions-split .abs-action-menu .action-submenu>li:active,.actions-split .action-menu .action-submenu>li:active,.actions-split .action-menu>li:active,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li:active,.actions-split .actions-split .dropdown-menu .action-submenu>li:active,.actions-split .dropdown-menu>li:active{background-color:#cacaca}.abs-action-menu>li._parent,.actions-split .abs-action-menu .action-submenu .action-submenu>li._parent,.actions-split .abs-action-menu .action-submenu>li._parent,.actions-split .action-menu .action-submenu>li._parent,.actions-split .action-menu>li._parent,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li._parent,.actions-split .actions-split .dropdown-menu .action-submenu>li._parent,.actions-split .dropdown-menu>li._parent{-webkit-flex-direction:row;display:flex;-ms-flex-direction:row;flex-direction:row}.abs-action-menu>li._parent>.action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .abs-action-menu .action-submenu>li._parent>.action-menu-item,.actions-split .action-menu .action-submenu>li._parent>.action-menu-item,.actions-split .action-menu>li._parent>.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu>li._parent>.action-menu-item,.actions-split .dropdown-menu>li._parent>.action-menu-item{min-width:100%}.abs-action-menu .action-menu-item,.abs-action-menu .item,.actions-split .abs-action-menu .action-submenu .action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu .action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu .item,.actions-split .abs-action-menu .action-submenu .item,.actions-split .action-menu .action-menu-item,.actions-split .action-menu .action-submenu .action-menu-item,.actions-split .action-menu .action-submenu .item,.actions-split .action-menu .item,.actions-split .actions-split .dropdown-menu .action-submenu .action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu .action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu .item,.actions-split .actions-split .dropdown-menu .action-submenu .item,.actions-split .dropdown-menu .action-menu-item,.actions-split .dropdown-menu .item{cursor:pointer;display:block;padding:.6875em 1em}.abs-action-menu .action-submenu,.actions-split .action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu{bottom:auto;left:auto;margin-left:0;margin-top:-1px;position:absolute;right:auto;top:auto}.ie9 .abs-action-menu .action-submenu,.ie9 .actions-split .abs-action-menu .action-submenu .action-submenu,.ie9 .actions-split .abs-action-menu .action-submenu .action-submenu .action-submenu,.ie9 .actions-split .action-menu .action-submenu,.ie9 .actions-split .action-menu .action-submenu .action-submenu,.ie9 .actions-split .actions-split .dropdown-menu .action-submenu .action-submenu,.ie9 .actions-split .actions-split .dropdown-menu .action-submenu .action-submenu .action-submenu,.ie9 .actions-split .dropdown-menu .action-submenu{margin-left:99%;margin-top:-3.5rem}.abs-action-menu a.action-menu-item,.actions-split .abs-action-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .abs-action-menu .action-submenu a.action-menu-item,.actions-split .action-menu .action-submenu a.action-menu-item,.actions-split .action-menu a.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .actions-split .dropdown-menu .action-submenu a.action-menu-item,.actions-split .dropdown-menu a.action-menu-item{color:#333}.abs-action-menu a.action-menu-item:focus,.actions-split .abs-action-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .abs-action-menu .action-submenu a.action-menu-item:focus,.actions-split .action-menu .action-submenu a.action-menu-item:focus,.actions-split .action-menu a.action-menu-item:focus,.actions-split .actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .actions-split .dropdown-menu .action-submenu a.action-menu-item:focus,.actions-split .dropdown-menu a.action-menu-item:focus{background-color:#e3e3e3;box-shadow:none}.abs-action-wrap-triangle{position:relative}.abs-action-wrap-triangle .action-default{width:100%}.abs-action-wrap-triangle .action-default:after,.abs-action-wrap-triangle .action-default:before{border-style:solid;content:'';height:0;position:absolute;top:0;width:0}.abs-action-wrap-triangle .action-default:active,.abs-action-wrap-triangle .action-default:focus,.abs-action-wrap-triangle .action-default:hover{box-shadow:none}._keyfocus .abs-action-wrap-triangle .action-default:focus{box-shadow:0 0 0 1px #007bdb}.ie10 .abs-action-wrap-triangle .action-default.disabled,.ie10 .abs-action-wrap-triangle .action-default[disabled],.ie9 .abs-action-wrap-triangle .action-default.disabled,.ie9 .abs-action-wrap-triangle .action-default[disabled]{background-color:#fcfcfc;opacity:1;text-shadow:none}.abs-action-wrap-triangle-right{display:inline-block;padding-right:1.6rem;position:relative}.abs-action-wrap-triangle-right .action-default:after,.abs-action-wrap-triangle-right .action-default:before{border-color:transparent transparent transparent #e3e3e3;border-width:1.7rem 0 1.6rem 1.7rem;left:100%;margin-left:-1.7rem}.abs-action-wrap-triangle-right .action-default:before{border-left-color:#949494;right:-1px}.abs-action-wrap-triangle-right .action-default:active:after,.abs-action-wrap-triangle-right .action-default:focus:after,.abs-action-wrap-triangle-right .action-default:hover:after{border-left-color:#dbdbdb}.ie10 .abs-action-wrap-triangle-right .action-default.disabled:after,.ie10 .abs-action-wrap-triangle-right .action-default[disabled]:after,.ie9 .abs-action-wrap-triangle-right .action-default.disabled:after,.ie9 .abs-action-wrap-triangle-right .action-default[disabled]:after{border-color:transparent transparent transparent #fcfcfc}.abs-action-wrap-triangle-right .action-primary:after{border-color:transparent transparent transparent #eb5202}.abs-action-wrap-triangle-right .action-primary:active:after,.abs-action-wrap-triangle-right .action-primary:focus:after,.abs-action-wrap-triangle-right .action-primary:hover:after{border-left-color:#ba4000}.abs-action-wrap-triangle-left{display:inline-block;padding-left:1.6rem}.abs-action-wrap-triangle-left .action-default{text-indent:-.85rem}.abs-action-wrap-triangle-left .action-default:after,.abs-action-wrap-triangle-left .action-default:before{border-color:transparent #e3e3e3 transparent transparent;border-width:1.7rem 1.7rem 1.6rem 0;margin-right:-1.7rem;right:100%}.abs-action-wrap-triangle-left .action-default:before{border-right-color:#949494;left:-1px}.abs-action-wrap-triangle-left .action-default:active:after,.abs-action-wrap-triangle-left .action-default:focus:after,.abs-action-wrap-triangle-left .action-default:hover:after{border-right-color:#dbdbdb}.ie10 .abs-action-wrap-triangle-left .action-default.disabled:after,.ie10 .abs-action-wrap-triangle-left .action-default[disabled]:after,.ie9 .abs-action-wrap-triangle-left .action-default.disabled:after,.ie9 .abs-action-wrap-triangle-left .action-default[disabled]:after{border-color:transparent #fcfcfc transparent transparent}.abs-action-wrap-triangle-left .action-primary:after{border-color:transparent #eb5202 transparent transparent}.abs-action-wrap-triangle-left .action-primary:active:after,.abs-action-wrap-triangle-left .action-primary:focus:after,.abs-action-wrap-triangle-left .action-primary:hover:after{border-right-color:#ba4000}.action-default,button{background:#e3e3e3;border-color:#adadad;color:#514943}.action-default:active,.action-default:focus,.action-default:hover,button:active,button:focus,button:hover{background-color:#dbdbdb;color:#514943;text-decoration:none}.action-primary{background-color:#eb5202;border-color:#eb5202;color:#fff;text-shadow:1px 1px 0 rgba(0,0,0,.25)}.action-primary:active,.action-primary:focus,.action-primary:hover{background-color:#ba4000;border-color:#b84002;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.action-primary.disabled,.action-primary[disabled]{cursor:default;opacity:.5;pointer-events:none}.action-secondary{background-color:#514943;border-color:#514943;color:#fff;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.action-secondary:active,.action-secondary:focus,.action-secondary:hover{background-color:#35302c;border-color:#35302c;box-shadow:0 0 0 1px #007bdb;color:#fff;text-decoration:none}.action-secondary:active{background-color:#35302c}.action-quaternary,.action-tertiary{background-color:transparent;border-color:transparent;text-shadow:none}.action-quaternary:active,.action-quaternary:focus,.action-quaternary:hover,.action-tertiary:active,.action-tertiary:focus,.action-tertiary:hover{background-color:transparent;border-color:transparent;box-shadow:none}.action-tertiary{color:#008bdb}.action-tertiary:active,.action-tertiary:focus,.action-tertiary:hover{color:#0fa7ff;text-decoration:underline}.action-quaternary{color:#333}.action-quaternary:active,.action-quaternary:focus,.action-quaternary:hover{color:#1a1a1a}.action-close>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.action-close:active{-ms-transform:scale(0.9);transform:scale(0.9)}.action-close:before{content:'\e62f';transition:color .1s linear}.action-close:hover{cursor:pointer;text-decoration:none}.abs-action-menu .action-submenu,.abs-action-menu .action-submenu .action-submenu,.action-menu,.action-menu .action-submenu,.actions-split .action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu,.actions-split .dropdown-menu .action-submenu .action-submenu{background-color:#fff;border:1px solid #007bdb;border-radius:1px;box-shadow:1px 1px 5px rgba(0,0,0,.5);color:#333;display:none;font-weight:400;left:0;list-style:none;margin:2px 0 0;min-width:0;padding:0;position:absolute;right:0;top:100%}.abs-action-menu .action-submenu .action-submenu._active,.abs-action-menu .action-submenu._active,.action-menu .action-submenu._active,.action-menu._active,.actions-split .action-menu .action-submenu .action-submenu._active,.actions-split .action-menu .action-submenu._active,.actions-split .dropdown-menu .action-submenu .action-submenu._active,.actions-split .dropdown-menu .action-submenu._active{display:block}.abs-action-menu .action-submenu .action-submenu>li,.abs-action-menu .action-submenu>li,.action-menu .action-submenu>li,.action-menu>li,.actions-split .action-menu .action-submenu .action-submenu>li,.actions-split .action-menu .action-submenu>li,.actions-split .dropdown-menu .action-submenu .action-submenu>li,.actions-split .dropdown-menu .action-submenu>li{border:none;display:block;padding:0;transition:background-color .1s linear}.abs-action-menu .action-submenu .action-submenu>li>a:hover,.abs-action-menu .action-submenu>li>a:hover,.action-menu .action-submenu>li>a:hover,.action-menu>li>a:hover,.actions-split .action-menu .action-submenu .action-submenu>li>a:hover,.actions-split .action-menu .action-submenu>li>a:hover,.actions-split .dropdown-menu .action-submenu .action-submenu>li>a:hover,.actions-split .dropdown-menu .action-submenu>li>a:hover{text-decoration:none}.abs-action-menu .action-submenu .action-submenu>li._visible,.abs-action-menu .action-submenu .action-submenu>li:hover,.abs-action-menu .action-submenu>li._visible,.abs-action-menu .action-submenu>li:hover,.action-menu .action-submenu>li._visible,.action-menu .action-submenu>li:hover,.action-menu>li._visible,.action-menu>li:hover,.actions-split .action-menu .action-submenu .action-submenu>li._visible,.actions-split .action-menu .action-submenu .action-submenu>li:hover,.actions-split .action-menu .action-submenu>li._visible,.actions-split .action-menu .action-submenu>li:hover,.actions-split .dropdown-menu .action-submenu .action-submenu>li._visible,.actions-split .dropdown-menu .action-submenu .action-submenu>li:hover,.actions-split .dropdown-menu .action-submenu>li._visible,.actions-split .dropdown-menu .action-submenu>li:hover{background-color:#e3e3e3}.abs-action-menu .action-submenu .action-submenu>li:active,.abs-action-menu .action-submenu>li:active,.action-menu .action-submenu>li:active,.action-menu>li:active,.actions-split .action-menu .action-submenu .action-submenu>li:active,.actions-split .action-menu .action-submenu>li:active,.actions-split .dropdown-menu .action-submenu .action-submenu>li:active,.actions-split .dropdown-menu .action-submenu>li:active{background-color:#cacaca}.abs-action-menu .action-submenu .action-submenu>li._parent,.abs-action-menu .action-submenu>li._parent,.action-menu .action-submenu>li._parent,.action-menu>li._parent,.actions-split .action-menu .action-submenu .action-submenu>li._parent,.actions-split .action-menu .action-submenu>li._parent,.actions-split .dropdown-menu .action-submenu .action-submenu>li._parent,.actions-split .dropdown-menu .action-submenu>li._parent{-webkit-flex-direction:row;display:flex;-ms-flex-direction:row;flex-direction:row}.abs-action-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.abs-action-menu .action-submenu>li._parent>.action-menu-item,.action-menu .action-submenu>li._parent>.action-menu-item,.action-menu>li._parent>.action-menu-item,.actions-split .action-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .action-menu .action-submenu>li._parent>.action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu>li._parent>.action-menu-item,.actions-split .dropdown-menu .action-submenu>li._parent>.action-menu-item{min-width:100%}.abs-action-menu .action-submenu .action-menu-item,.abs-action-menu .action-submenu .action-submenu .action-menu-item,.abs-action-menu .action-submenu .action-submenu .item,.abs-action-menu .action-submenu .item,.action-menu .action-menu-item,.action-menu .action-submenu .action-menu-item,.action-menu .action-submenu .item,.action-menu .item,.actions-split .action-menu .action-submenu .action-menu-item,.actions-split .action-menu .action-submenu .action-submenu .action-menu-item,.actions-split .action-menu .action-submenu .action-submenu .item,.actions-split .action-menu .action-submenu .item,.actions-split .dropdown-menu .action-submenu .action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu .action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu .item,.actions-split .dropdown-menu .action-submenu .item{cursor:pointer;display:block;padding:.6875em 1em}.abs-action-menu .action-submenu .action-submenu,.action-menu .action-submenu,.actions-split .action-menu .action-submenu .action-submenu,.actions-split .dropdown-menu .action-submenu .action-submenu{bottom:auto;left:auto;margin-left:0;margin-top:-1px;position:absolute;right:auto;top:auto}.ie9 .abs-action-menu .action-submenu .action-submenu,.ie9 .abs-action-menu .action-submenu .action-submenu .action-submenu,.ie9 .action-menu .action-submenu,.ie9 .action-menu .action-submenu .action-submenu,.ie9 .actions-split .action-menu .action-submenu .action-submenu,.ie9 .actions-split .action-menu .action-submenu .action-submenu .action-submenu,.ie9 .actions-split .dropdown-menu .action-submenu .action-submenu,.ie9 .actions-split .dropdown-menu .action-submenu .action-submenu .action-submenu{margin-left:99%;margin-top:-3.5rem}.abs-action-menu .action-submenu .action-submenu a.action-menu-item,.abs-action-menu .action-submenu a.action-menu-item,.action-menu .action-submenu a.action-menu-item,.action-menu a.action-menu-item,.actions-split .action-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .action-menu .action-submenu a.action-menu-item,.actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item,.actions-split .dropdown-menu .action-submenu a.action-menu-item{color:#333}.abs-action-menu .action-submenu .action-submenu a.action-menu-item:focus,.abs-action-menu .action-submenu a.action-menu-item:focus,.action-menu .action-submenu a.action-menu-item:focus,.action-menu a.action-menu-item:focus,.actions-split .action-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .action-menu .action-submenu a.action-menu-item:focus,.actions-split .dropdown-menu .action-submenu .action-submenu a.action-menu-item:focus,.actions-split .dropdown-menu .action-submenu a.action-menu-item:focus{background-color:#e3e3e3;box-shadow:none}.messages .message:last-child{margin:0 0 2rem}.message{background:#fffbbb;border:none;border-radius:0;color:#333;font-size:1.4rem;margin:0 0 1px;padding:1.8rem 4rem 1.8rem 5.5rem;position:relative;text-shadow:none}.message:before{background:0 0;border:0;color:#007bdb;content:'\e61a';font-family:Icons;font-size:1.9rem;font-style:normal;font-weight:400;height:auto;left:1.9rem;line-height:inherit;margin-top:-1.3rem;position:absolute;speak:none;text-shadow:none;top:50%;width:auto}.message-notice:before{color:#007bdb;content:'\e61a'}.message-warning:before{color:#eb5202;content:'\e623'}.message-error{background:#fcc}.message-error:before{color:#e22626;content:'\e632';font-size:1.5rem;left:2.2rem;margin-top:-1rem}.message-success:before{color:#79a22e;content:'\e62d'}.message-spinner:before{display:none}.message-spinner .spinner{font-size:2.5rem;left:1.5rem;position:absolute;top:1.5rem}.message-in-rating-edit{margin-left:1.8rem;margin-right:1.8rem}.modal-popup .action-close,.modal-slide .action-close{color:#736963;position:absolute;right:0;top:0;z-index:1}.modal-popup .action-close:active,.modal-slide .action-close:active{-ms-transform:none;transform:none}.modal-popup .action-close:active:before,.modal-slide .action-close:active:before{font-size:1.8rem}.modal-popup .action-close:hover:before,.modal-slide .action-close:hover:before{color:#58504b}.modal-popup .action-close:before,.modal-slide .action-close:before{font-size:2rem}.modal-popup .action-close:focus,.modal-slide .action-close:focus{background-color:transparent}.modal-popup.prompt .prompt-message{padding:2rem 0}.modal-popup.prompt .prompt-message input{width:100%}.modal-popup.confirm .modal-inner-wrap .message,.modal-popup.prompt .modal-inner-wrap .message{background:#fff}.modal-popup.modal-system-messages .modal-inner-wrap{background:#fffbbb}.modal-popup._image-box .modal-inner-wrap{margin:5rem auto;max-width:78rem;position:static}.modal-popup._image-box .thumbnail-preview{padding-bottom:3rem;text-align:center}.modal-popup._image-box .thumbnail-preview .thumbnail-preview-image-block{border:1px solid #ccc;margin:0 auto 2rem;max-width:58rem;padding:2rem}.modal-popup._image-box .thumbnail-preview .thumbnail-preview-image{max-height:54rem}.modal-popup .modal-title{font-size:2.4rem;margin-right:6.4rem}.modal-popup .modal-footer{padding-top:2.6rem;text-align:right}.modal-popup .action-close{padding:3rem}.modal-popup .action-close:active,.modal-popup .action-close:focus{background:0 0;padding-right:3.1rem;padding-top:3.1rem}.modal-slide .modal-content-new-attribute{-webkit-overflow-scrolling:touch;overflow:auto;padding-bottom:0}.modal-slide .modal-content-new-attribute iframe{margin-bottom:-2.5rem}.modal-slide .modal-title{font-size:2.1rem;margin-right:5.7rem}.modal-slide .action-close{padding:2.1rem 2.6rem}.modal-slide .action-close:active{padding-right:2.7rem;padding-top:2.2rem}.modal-slide .page-main-actions{margin-bottom:.6rem;margin-top:2.1rem}.modal-slide .magento-message{padding:0 3rem 3rem;position:relative}.modal-slide .magento-message .insert-title-inner,.modal-slide .main-col .insert-title-inner{border-bottom:1px solid #adadad;margin:0 0 2rem;padding-bottom:.5rem}.modal-slide .magento-message .insert-actions,.modal-slide .main-col .insert-actions{float:right}.modal-slide .magento-message .title,.modal-slide .main-col .title{font-size:1.6rem;padding-top:.5rem}.modal-slide .main-col,.modal-slide .side-col{float:left;padding-bottom:0}.modal-slide .main-col:after,.modal-slide .side-col:after{display:none}.modal-slide .side-col{width:20%}.modal-slide .main-col{padding-right:0;width:80%}.modal-slide .content-footer .form-buttons{float:right}.modal-title{font-weight:400;margin-bottom:0;min-height:1em}.modal-title span{font-size:1.4rem;font-style:italic;margin-left:1rem}.spinner{display:inline-block;font-size:4rem;height:1em;margin-right:1.5rem;position:relative;width:1em}.spinner>span:nth-child(1){animation-delay:.27s;-ms-transform:rotate(-315deg);transform:rotate(-315deg)}.spinner>span:nth-child(2){animation-delay:.36s;-ms-transform:rotate(-270deg);transform:rotate(-270deg)}.spinner>span:nth-child(3){animation-delay:.45s;-ms-transform:rotate(-225deg);transform:rotate(-225deg)}.spinner>span:nth-child(4){animation-delay:.54s;-ms-transform:rotate(-180deg);transform:rotate(-180deg)}.spinner>span:nth-child(5){animation-delay:.63s;-ms-transform:rotate(-135deg);transform:rotate(-135deg)}.spinner>span:nth-child(6){animation-delay:.72s;-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.spinner>span:nth-child(7){animation-delay:.81s;-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.spinner>span:nth-child(8){animation-delay:.9;-ms-transform:rotate(0deg);transform:rotate(0deg)}@keyframes fade{0%{background-color:#514943}100%{background-color:#fff}}.spinner>span{-ms-transform:scale(0.4);transform:scale(0.4);animation-name:fade;animation-duration:.72s;animation-iteration-count:infinite;animation-direction:linear;background-color:#fff;border-radius:6px;clip:rect(0 .28571429em .1em 0);height:.1em;margin-top:.5em;position:absolute;width:1em}.ie9 .spinner{background:url(../images/ajax-loader.gif) center no-repeat}.ie9 .spinner>span{display:none}.popup-loading{background:rgba(255,255,255,.8);border-color:#ef672f;color:#ef672f;font-size:14px;font-weight:700;left:50%;margin-left:-100px;padding:100px 0 10px;position:fixed;text-align:center;top:40%;width:200px;z-index:1003}.popup-loading:after{background-image:url(../images/loader-1.gif);content:'';height:64px;left:50%;margin:-32px 0 0 -32px;position:absolute;top:40%;width:64px;z-index:2}.loading-mask,.loading-old{background:rgba(255,255,255,.4);bottom:0;left:0;position:fixed;right:0;top:0;z-index:2003}.loading-mask img,.loading-old img{display:none}.loading-mask p,.loading-old p{margin-top:118px}.loading-mask .loader,.loading-old .loader{background:url(../images/loader-1.gif) 50% 30% no-repeat #f7f3eb;border-radius:5px;bottom:0;color:#575757;font-size:14px;font-weight:700;height:160px;left:0;margin:auto;opacity:.95;position:absolute;right:0;text-align:center;top:0;width:160px}.admin-user{float:right;line-height:1.36;margin-left:.3rem;z-index:490}.admin-user._active .admin__action-dropdown,.admin-user.active .admin__action-dropdown{border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.admin-user .admin__action-dropdown{height:3.3rem;padding:.7rem 2.8rem .4rem 4rem}.admin-user .admin__action-dropdown._active:after,.admin-user .admin__action-dropdown.active:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin-user .admin__action-dropdown:after{border-color:#777 transparent transparent;border-style:solid;border-width:.5rem .4rem 0;content:'';height:0;margin-top:-.2rem;position:absolute;right:1.3rem;top:50%;transition:all .2s linear;width:0}._active .admin-user .admin__action-dropdown:after,.active .admin-user .admin__action-dropdown:after{-ms-transform:rotate(180deg);transform:rotate(180deg)}.admin-user .admin__action-dropdown:hover:after{border-color:#000 transparent transparent}.admin-user .admin__action-dropdown:before{color:#777;content:'\e600';font-size:2rem;left:1.1rem;margin-top:-1.1rem;position:absolute;top:50%}.admin-user .admin__action-dropdown:hover:before{color:#333}.admin-user .admin__action-dropdown-menu{min-width:20rem;padding-left:1rem;padding-right:1rem}.admin-user .admin__action-dropdown-menu>li>a{padding-left:.5em;padding-right:1.8rem;transition:background-color .1s linear;white-space:nowrap}.admin-user .admin__action-dropdown-menu>li>a:hover{background-color:#e0f6fe;color:#333}.admin-user .admin__action-dropdown-menu>li>a:active{background-color:#c7effd;bottom:-1px;position:relative}.admin-user .admin__action-dropdown-menu .admin-user-name{text-overflow:ellipsis;white-space:nowrap;display:inline-block;max-width:20rem;overflow:hidden;vertical-align:top}.admin-user-account-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;max-width:11.2rem}.search-global{float:right;margin-right:-.3rem;position:relative;z-index:480}.search-global-field{min-width:5rem}.search-global-field._active .search-global-input{background-color:#fff;border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5);padding-right:4rem;width:25rem}.search-global-field._active .search-global-action{display:block;height:3.3rem;position:absolute;right:0;text-indent:-100%;top:0;width:5rem;z-index:3}.search-global-field .autocomplete-results{height:3.3rem;position:absolute;right:0;top:0;width:25rem}.search-global-field .search-global-menu{border:1px solid #007bdb;border-top-color:transparent;box-shadow:1px 1px 5px rgba(0,0,0,.5);left:0;margin-top:-2px;padding:0;position:absolute;right:0;top:100%;z-index:2}.search-global-field .search-global-menu:after{background-color:#fff;content:'';height:5px;left:0;position:absolute;right:0;top:-5px}.search-global-field .search-global-menu>li{background-color:#fff;border-top:1px solid #ddd;display:block;font-size:1.2rem;padding:.75rem 1.4rem .55rem}.search-global-field .search-global-menu>li._active{background-color:#e0f6fe}.search-global-field .search-global-menu .title{display:block;font-size:1.4rem}.search-global-field .search-global-menu .type{color:#1a1a1a;display:block}.search-global-label{cursor:pointer;height:3.3rem;padding:.75rem 1.4rem .55rem;position:absolute;right:0;top:0;z-index:2}.search-global-label:active{-ms-transform:scale(0.9);transform:scale(0.9)}.search-global-label:hover:before{color:#000}.search-global-label:before{color:#777;content:'\e60c';font-size:2rem}.search-global-input{background-color:transparent;border:1px solid transparent;font-size:1.4rem;height:3.3rem;padding:.75rem 1.4rem .55rem;position:absolute;right:0;top:0;transition:all .1s linear,width .3s linear;width:5rem;z-index:1}.search-global-action{display:none}.notifications-wrapper{float:right;line-height:1;position:relative}.notifications-wrapper.active{z-index:500}.notifications-wrapper.active .notifications-action{border-color:#007bdb;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.notifications-wrapper.active .notifications-action:after{background-color:#fff;border:none;content:'';display:block;height:6px;left:-6px;margin-top:0;position:absolute;right:0;top:100%;width:auto}.notifications-wrapper .admin__action-dropdown-menu{padding:1rem 0 0;width:32rem}.notifications-action{color:#777;height:3.3rem;padding:.75rem 2rem .65rem}.notifications-action:after{display:none}.notifications-action:before{content:'\e607';font-size:1.9rem;margin-right:0}.notifications-action:active:before{position:relative;top:1px}.notifications-action .notifications-counter{background-color:#e22626;border-radius:1em;color:#fff;display:inline-block;font-size:1.1rem;font-weight:700;left:50%;margin-left:.3em;margin-top:-1.1em;padding:.3em .5em;position:absolute;top:50%}.notifications-entry{line-height:1.36;padding:.6rem 2rem .8rem;position:relative;transition:background-color .1s linear}.notifications-entry:hover{background-color:#e0f6fe}.notifications-entry.notifications-entry-last{margin:0 2rem;padding:.3rem 0 1.3rem;text-align:center}.notifications-entry.notifications-entry-last:hover{background-color:transparent}.notifications-entry+.notifications-entry-last{border-top:1px solid #ddd;padding-bottom:.6rem}.notifications-entry ._cutted{cursor:pointer}.notifications-entry ._cutted .notifications-entry-description-start:after{content:'...'}.notifications-entry-title{color:#ef672f;display:block;font-size:1.1rem;font-weight:700;margin-bottom:.7rem;margin-right:1em}.notifications-entry-description{color:#333;font-size:1.1rem;margin-bottom:.8rem}.notifications-entry-description-end{display:none}.notifications-entry-description-end._show{display:inline}.notifications-entry-time{color:#777;font-size:1.1rem}.notifications-close{line-height:1;padding:1rem;position:absolute;right:0;top:.6rem}.notifications-close:before{color:#ccc;content:'\e620';transition:color .1s linear}.notifications-close:hover:before{color:#b3b3b3}.notifications-close:active{-ms-transform:scale(0.95);transform:scale(0.95)}.page-header-actions{padding-top:1.1rem}.page-header-hgroup{padding-right:1.5rem}.page-title{color:#333;font-size:2.8rem}.page-header{padding:1.5rem 3rem}.menu-wrapper{display:inline-block;position:relative;width:8.8rem;z-index:700}.menu-wrapper:before{background-color:#373330;bottom:0;content:'';left:0;position:fixed;top:0;width:8.8rem;z-index:699}.menu-wrapper._fixed{left:0;position:fixed;top:0}.menu-wrapper._fixed~.page-wrapper{margin-left:8.8rem}.menu-wrapper .logo{display:block;height:8.8rem;padding:2.4rem 0 2.2rem;position:relative;text-align:center;z-index:700}._keyfocus .menu-wrapper .logo:focus{background-color:#4a4542;box-shadow:none}._keyfocus .menu-wrapper .logo:focus+.admin__menu .level-0:first-child>a{background-color:#373330}._keyfocus .menu-wrapper .logo:focus+.admin__menu .level-0:first-child>a:after{display:none}.menu-wrapper .logo:hover .logo-img{-webkit-filter:brightness(1.1);filter:brightness(1.1)}.menu-wrapper .logo:active .logo-img{-ms-transform:scale(0.95);transform:scale(0.95)}.menu-wrapper .logo .logo-img{height:4.2rem;transition:-webkit-filter .2s linear,filter .2s linear,transform .1s linear;width:3.5rem}.abs-menu-separator,.admin__menu .item-partners>a:after,.admin__menu .level-0:first-child>a:after{background-color:#736963;content:'';display:block;height:1px;left:0;margin-left:16%;position:absolute;top:0;width:68%}.admin__menu li{display:block}.admin__menu .level-0:first-child>a{position:relative}.admin__menu .level-0._active>a,.admin__menu .level-0:hover>a{color:#f7f3eb}.admin__menu .level-0._active>a{background-color:#524d49}.admin__menu .level-0:hover>a{background-color:#4a4542}.admin__menu .level-0>a{color:#aaa6a0;display:block;font-size:1rem;letter-spacing:.025em;min-height:6.2rem;padding:1.2rem .5rem .5rem;position:relative;text-align:center;text-decoration:none;text-transform:uppercase;transition:background-color .1s linear;word-wrap:break-word;z-index:700}.admin__menu .level-0>a:focus{box-shadow:none}.admin__menu .level-0>a:before{content:'\e63a';display:block;font-size:2.2rem;height:2.2rem}.admin__menu .level-0>.submenu{background-color:#4a4542;box-shadow:0 0 3px #000;left:100%;min-height:calc(8.8rem + 2rem + 100%);padding:2rem 0 0;position:absolute;top:0;-ms-transform:translateX(-100%);transform:translateX(-100%);transition-duration:.3s;transition-property:transform,visibility;transition-timing-function:ease-in-out;visibility:hidden;z-index:697}.ie10 .admin__menu .level-0>.submenu,.ie11 .admin__menu .level-0>.submenu{height:100%}.admin__menu .level-0._show>.submenu{-ms-transform:translateX(0);transform:translateX(0);visibility:visible;z-index:698}.admin__menu .level-1{margin-left:1.5rem;margin-right:1.5rem}.admin__menu [class*=level-]:not(.level-0) a{display:block;padding:1.25rem 1.5rem}.admin__menu [class*=level-]:not(.level-0) a:hover{background-color:#403934}.admin__menu [class*=level-]:not(.level-0) a:active{background-color:#322c29;padding-bottom:1.15rem;padding-top:1.35rem}.admin__menu .submenu li{min-width:23.8rem}.admin__menu .submenu a{color:#fcfcfc;transition:background-color .1s linear}.admin__menu .submenu a:focus,.admin__menu .submenu a:hover{box-shadow:none;text-decoration:none}._keyfocus .admin__menu .submenu a:focus{background-color:#403934}._keyfocus .admin__menu .submenu a:active{background-color:#322c29}.admin__menu .submenu .parent{margin-bottom:4.5rem}.admin__menu .submenu .parent .submenu-group-title{color:#a79d95;display:block;font-size:1.6rem;font-weight:600;margin-bottom:.7rem;padding:1.25rem 1.5rem;pointer-events:none}.admin__menu .submenu .column{display:table-cell}.admin__menu .submenu-title{color:#fff;display:block;font-size:2.2rem;font-weight:600;margin-bottom:4.2rem;margin-left:3rem;margin-right:5.8rem}.admin__menu .submenu-sub-title{color:#fff;display:block;font-size:1.2rem;margin:-3.8rem 5.8rem 3.8rem 3rem}.admin__menu .action-close{padding:2.4rem 2.8rem;position:absolute;right:0;top:0}.admin__menu .action-close:before{color:#a79d95;font-size:1.7rem}.admin__menu .action-close:hover:before{color:#fff}.admin__menu .item-dashboard>a:before{content:'\e604';font-size:1.8rem;padding-top:.4rem}.admin__menu .item-sales>a:before{content:'\e60b'}.admin__menu .item-catalog>a:before{content:'\e608'}.admin__menu .item-customer>a:before{content:'\e603';font-size:2.6rem;position:relative;top:-.4rem}.admin__menu .item-marketing>a:before{content:'\e609';font-size:2rem;padding-top:.2rem}.admin__menu .item-content>a:before{content:'\e602';font-size:2.4rem;position:relative;top:-.2rem}.admin__menu .item-report>a:before{content:'\e60a'}.admin__menu .item-stores>a:before{content:'\e60d';font-size:1.9rem;padding-top:.3rem}.admin__menu .item-system>a:before{content:'\e610'}.admin__menu .item-partners._active>a:after,.admin__menu .item-system._current+.item-partners>a:after{display:none}.admin__menu .item-partners>a{padding-bottom:1rem}.admin__menu .item-partners>a:before{content:'\e612'}.admin__menu .level-0>.submenu>ul>.level-1:only-of-type>.submenu-group-title,.admin__menu .submenu .column:only-of-type .submenu-group-title{display:none}.admin__menu-overlay{bottom:0;left:0;position:fixed;right:0;top:0;z-index:697}.store-switcher{color:#333;float:left;font-size:1.3rem;margin-top:.7rem}.store-switcher .admin__action-dropdown{background-color:#f8f8f8;margin-left:.5em}.store-switcher .dropdown{display:inline-block;position:relative}.store-switcher .dropdown:after,.store-switcher .dropdown:before{content:'';display:table}.store-switcher .dropdown:after{clear:both}.store-switcher .dropdown .action.toggle{cursor:pointer;display:inline-block;text-decoration:none}.store-switcher .dropdown .action.toggle:after{-webkit-font-smoothing:antialiased;font-size:22px;line-height:2;color:#333;content:'\e607';font-family:icons-blank-theme;margin:0;vertical-align:top;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.store-switcher .dropdown .action.toggle:active:after,.store-switcher .dropdown .action.toggle:hover:after{color:#333}.store-switcher .dropdown .action.toggle.active{display:inline-block;text-decoration:none}.store-switcher .dropdown .action.toggle.active:after{-webkit-font-smoothing:antialiased;font-size:22px;line-height:2;color:#333;content:'\e618';font-family:icons-blank-theme;margin:0;vertical-align:top;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.store-switcher .dropdown .action.toggle.active:active:after,.store-switcher .dropdown .action.toggle.active:hover:after{color:#333}.store-switcher .dropdown .dropdown-menu{margin:4px 0 0;padding:0;list-style:none;background:#fff;border:1px solid #aaa6a0;min-width:19.5rem;z-index:100;box-sizing:border-box;display:none;position:absolute;top:100%;box-shadow:1px 1px 5px rgba(0,0,0,.5)}.store-switcher .dropdown .dropdown-menu li{margin:0;padding:0}.store-switcher .dropdown .dropdown-menu li:hover{background:0 0;cursor:pointer}.store-switcher .dropdown.active{overflow:visible}.store-switcher .dropdown.active .dropdown-menu{display:block}.store-switcher .dropdown-menu{left:0;margin-top:.5em;max-height:250px;overflow-y:auto;padding-top:.25em}.store-switcher .dropdown-menu li{border:0;cursor:default}.store-switcher .dropdown-menu li:hover{cursor:default}.store-switcher .dropdown-menu li a,.store-switcher .dropdown-menu li span{color:#333;display:block;padding:.5rem 1.3rem}.store-switcher .dropdown-menu li a{text-decoration:none}.store-switcher .dropdown-menu li a:hover{background:#e9e9e9}.store-switcher .dropdown-menu li span{color:#adadad;cursor:default}.store-switcher .dropdown-menu li.current span{background:#eee;color:#333}.store-switcher .dropdown-menu .store-switcher-store a,.store-switcher .dropdown-menu .store-switcher-store span{padding-left:2.6rem}.store-switcher .dropdown-menu .store-switcher-store-view a,.store-switcher .dropdown-menu .store-switcher-store-view span{padding-left:3.9rem}.store-switcher .dropdown-menu .dropdown-toolbar{border-top:1px solid #ebebeb;margin-top:1rem}.store-switcher .dropdown-menu .dropdown-toolbar a:before{content:'\e610';margin-right:.25em;position:relative;top:1px}.store-switcher-label{font-weight:700}.store-switcher-alt{display:inline-block;position:relative}.store-switcher-alt.active .dropdown-menu{display:block}.store-switcher-alt .dropdown-menu{margin-top:2px;white-space:nowrap}.store-switcher-alt .dropdown-menu ul{list-style:none;margin:0;padding:0}.store-switcher-alt strong{color:#a79d95;display:block;font-size:14px;font-weight:500;line-height:1.333;padding:5px 10px}.store-switcher-alt .store-selected{color:#676056;cursor:pointer;font-size:12px;font-weight:400;line-height:1.333}.store-switcher-alt .store-selected:after{-webkit-font-smoothing:antialiased;color:#afadac;content:'\e02c';font-style:normal;font-weight:400;margin:0 0 0 3px;speak:none;vertical-align:text-top}.store-switcher-alt .store-switcher-store,.store-switcher-alt .store-switcher-website{padding:0}.store-switcher-alt .store-switcher-store:hover,.store-switcher-alt .store-switcher-website:hover{background:0 0}.store-switcher-alt .manage-stores,.store-switcher-alt .store-switcher-all,.store-switcher-alt .store-switcher-store-view{padding:0}.store-switcher-alt .manage-stores>a,.store-switcher-alt .store-switcher-all>a{color:#676056;display:block;font-size:12px;padding:8px 15px;text-decoration:none}.store-switcher-website{margin:5px 0 0}.store-switcher-website>strong{padding-left:13px}.store-switcher-store{margin:1px 0 0}.store-switcher-store>strong{padding-left:20px}.store-switcher-store>ul{margin-top:1px}.store-switcher-store-view:first-child{border-top:1px solid #e5e5e5}.store-switcher-store-view>a{color:#333;display:block;font-size:13px;padding:5px 15px 5px 24px;text-decoration:none}.store-view:not(.store-switcher){float:left}.store-view .store-switcher-label{display:inline-block;margin-top:1rem}.tooltip{margin-left:.5em}.tooltip .help a,.tooltip .help span{cursor:pointer;display:inline-block;height:22px;position:relative;vertical-align:middle;width:22px;z-index:2}.tooltip .help a:before,.tooltip .help span:before{color:#333;content:'\e633';font-size:1.7rem}.tooltip .help a:hover{text-decoration:none}.tooltip .tooltip-content{background:#000;border-radius:3px;color:#fff;display:none;margin-left:-19px;margin-top:10px;max-width:200px;padding:4px 8px;position:absolute;text-shadow:none;z-index:20}.tooltip .tooltip-content:before{border-bottom:5px solid #000;border-left:5px solid transparent;border-right:5px solid transparent;content:'';height:0;left:20px;opacity:.8;position:absolute;top:-5px;width:0}.tooltip .tooltip-content.loading{position:absolute}.tooltip .tooltip-content.loading:before{border-bottom-color:rgba(0,0,0,.3)}.tooltip:hover>.tooltip-content{display:block}.page-actions._fixed,.page-main-actions:not(._hidden){background:#f8f8f8;border-bottom:1px solid #e3e3e3;border-top:1px solid #e3e3e3;padding:1.5rem}.page-main-actions{margin:0 0 3rem}.page-main-actions._hidden .store-switcher{display:none}.page-main-actions._hidden .page-actions-placeholder{min-height:50px}.page-actions{float:right}.page-main-actions .page-actions._fixed{left:8.8rem;position:fixed;right:0;top:0;z-index:501}.page-main-actions .page-actions._fixed .page-actions-inner:before{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#333;content:attr(data-title);float:left;font-size:2.8rem;margin-top:.3rem;max-width:50%}.page-actions .page-actions-buttons>button,.page-actions>button{float:right;margin-left:1.3rem}.page-actions .page-actions-buttons>button.action-back,.page-actions .page-actions-buttons>button.back,.page-actions>button.action-back,.page-actions>button.back{float:left;-ms-flex-order:-1;order:-1}.page-actions .page-actions-buttons>button.action-back:before,.page-actions .page-actions-buttons>button.back:before,.page-actions>button.action-back:before,.page-actions>button.back:before{content:'\e626';margin-right:.5em;position:relative;top:1px}.page-actions .page-actions-buttons>button.action-primary,.page-actions .page-actions-buttons>button.primary,.page-actions>button.action-primary,.page-actions>button.primary{-ms-flex-order:2;order:2}.page-actions .page-actions-buttons>button.save:not(.primary),.page-actions>button.save:not(.primary){-ms-flex-order:1;order:1}.page-actions .page-actions-buttons>button.delete,.page-actions>button.delete{-ms-flex-order:-1;order:-1}.page-actions .actions-split{float:right;margin-left:1.3rem;-ms-flex-order:2;order:2}.page-actions .actions-split .dropdown-menu .item{display:block}.page-actions-buttons{float:right;-ms-flex-pack:end;justify-content:flex-end;display:-ms-flexbox;display:flex}.customer-index-edit .page-actions-buttons{background-color:transparent}.admin__page-nav{background:#f1f1f1;border:1px solid #e3e3e3}.admin__page-nav._collapsed:first-child{border-bottom:none}.admin__page-nav._collapsed._show{border-bottom:1px solid #e3e3e3}.admin__page-nav._collapsed._show ._collapsible{background:#f1f1f1}.admin__page-nav._collapsed._show ._collapsible:after{content:'\e62b'}.admin__page-nav._collapsed._show ._collapsible+.admin__page-nav-items{display:block}.admin__page-nav._collapsed._hide .admin__page-nav-title-messages,.admin__page-nav._collapsed._hide .admin__page-nav-title-messages ._active{display:inline-block}.admin__page-nav+._collapsed{border-bottom:none;border-top:none}.admin__page-nav-title{border-bottom:1px solid #e3e3e3;color:#303030;display:block;font-size:1.4rem;line-height:1.2;margin:0 0 -1px;padding:1.8rem 1.5rem;position:relative;text-transform:uppercase}.admin__page-nav-title._collapsible{background:#fff;cursor:pointer;margin:0;padding-right:3.5rem;transition:border-color .1s ease-out,background-color .1s ease-out}.admin__page-nav-title._collapsible+.admin__page-nav-items{display:none;margin-top:-1px}.admin__page-nav-title._collapsible:after{content:'\e628';font-size:1.3rem;font-weight:700;position:absolute;right:1.8rem;top:2rem}.admin__page-nav-title._collapsible:hover{background:#f1f1f1}.admin__page-nav-title._collapsible:last-child{margin:0 0 -1px}.admin__page-nav-title strong{font-weight:700}.admin__page-nav-title .admin__page-nav-title-messages{display:none}.admin__page-nav-items{list-style-type:none;margin:0;padding:1rem 0 1.3rem}.admin__page-nav-item{border-left:3px solid transparent;margin-left:.7rem;padding:0;position:relative;transition:border-color .1s ease-out,background-color .1s ease-out}.admin__page-nav-item:hover{border-color:#e4e4e4}.admin__page-nav-item:hover .admin__page-nav-link{background:#e4e4e4;color:#303030;text-decoration:none}.admin__page-nav-item._active,.admin__page-nav-item.ui-state-active{border-color:#eb5202}.admin__page-nav-item._active .admin__page-nav-link,.admin__page-nav-item.ui-state-active .admin__page-nav-link{background:#fff;border-color:#e3e3e3;border-right:1px solid #fff;color:#303030;margin-right:-1px;font-weight:600}.admin__page-nav-item._loading:before,.admin__page-nav-item.ui-tabs-loading:before{display:none}.admin__page-nav-item._loading .admin__page-nav-item-message-loader,.admin__page-nav-item.ui-tabs-loading .admin__page-nav-item-message-loader{display:inline-block}.admin__page-nav-link{border:1px solid transparent;border-width:1px 0;color:#303030;display:block;font-weight:500;line-height:1.2;margin:0 0 -1px;padding:2rem 4rem 2rem 1rem;transition:border-color .1s ease-out,background-color .1s ease-out;word-wrap:break-word}.admin__page-nav-item-messages{display:inline-block}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip{background:#f1f1f1;border:1px solid #f1f1f1;border-radius:1px;bottom:3.7rem;box-shadow:0 3px 9px 0 rgba(0,0,0,.3);display:none;font-size:1.4rem;font-weight:400;left:-1rem;line-height:1.36;padding:1.5rem;position:absolute;text-transform:none;width:27rem;word-break:normal;z-index:2}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:after,.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:before{border:15px solid transparent;height:0;width:0;border-top-color:#f1f1f1;content:'';display:block;left:2rem;position:absolute;top:100%;z-index:3}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:after{border-top-color:#f1f1f1;margin-top:-1px;z-index:4}.admin__page-nav-item-messages .admin__page-nav-item-message-tooltip:before{border-top-color:#bfbfbf;margin-top:1px}.admin__page-nav-item-message-loader{display:none;margin-top:-1rem;position:absolute;right:0;top:50%}.admin__page-nav-item-message-loader .spinner{font-size:2rem;margin-right:1.5rem}._loading>.admin__page-nav-item-messages .admin__page-nav-item-message-loader{display:inline-block}.admin__page-nav-item-message{position:relative}.admin__page-nav-item-message:hover{z-index:500}.admin__page-nav-item-message:hover .admin__page-nav-item-message-tooltip{display:block}.admin__page-nav-item-message._changed,.admin__page-nav-item-message._error{display:none}.admin__page-nav-item-message .admin__page-nav-item-message-icon{display:inline-block;font-size:1.4rem;padding-left:.8em;vertical-align:baseline}.admin__page-nav-item-message .admin__page-nav-item-message-icon:after{color:#666;content:'\e631'}._changed:not(._error)>.admin__page-nav-item-messages ._changed{display:inline-block}._error .admin__page-nav-item-message-icon:after{color:#eb5202;content:'\e623'}._error>.admin__page-nav-item-messages ._error{display:inline-block}._error>.admin__page-nav-item-messages ._error .spinner{font-size:2rem;margin-right:1.5rem}._error .admin__page-nav-item-message-tooltip{background:#f1f1f1;border:1px solid #f1f1f1;border-radius:1px;bottom:3.7rem;box-shadow:0 3px 9px 0 rgba(0,0,0,.3);display:none;font-weight:400;left:-1rem;line-height:1.36;padding:2rem;position:absolute;text-transform:none;width:27rem;word-break:normal;z-index:2}._error .admin__page-nav-item-message-tooltip:after,._error .admin__page-nav-item-message-tooltip:before{border:15px solid transparent;height:0;width:0;border-top-color:#f1f1f1;content:'';display:block;left:2rem;position:absolute;top:100%;z-index:3}._error .admin__page-nav-item-message-tooltip:after{border-top-color:#f1f1f1;margin-top:-1px;z-index:4}._error .admin__page-nav-item-message-tooltip:before{border-top-color:#bfbfbf}.admin__data-grid-wrap-static .data-grid{box-sizing:border-box}.admin__data-grid-wrap-static .data-grid thead{color:#333}.admin__data-grid-wrap-static .data-grid tr:nth-child(even) td{background-color:#f5f5f5}.admin__data-grid-wrap-static .data-grid tr:nth-child(even) td._dragging{background-color:rgba(245,245,245,.95)}.admin__data-grid-wrap-static .data-grid ul{margin-left:1rem;padding-left:1rem}.admin__data-grid-wrap-static .admin__data-grid-loading-mask{background:rgba(255,255,255,.5);bottom:0;left:0;position:absolute;right:0;top:0;z-index:399}.admin__data-grid-wrap-static .admin__data-grid-loading-mask .grid-loader{background:url(../images/loader-2.gif) 50% 50% no-repeat;bottom:0;height:149px;left:0;margin:auto;position:absolute;right:0;top:0;width:218px}.data-grid-filters-actions-wrap{float:right}.data-grid-search-control-wrap{float:left;max-width:45.5rem;position:relative;width:35%}.data-grid-search-control-wrap :-ms-input-placeholder{font-style:italic}.data-grid-search-control-wrap ::-webkit-input-placeholder{font-style:italic}.data-grid-search-control-wrap ::-moz-placeholder{font-style:italic}.data-grid-search-control-wrap .action-submit{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;padding:.6rem 2rem .2rem;position:absolute;right:0;top:1px}.data-grid-search-control-wrap .action-submit:hover{background-color:transparent;border:none;box-shadow:none}.data-grid-search-control-wrap .action-submit:active{-ms-transform:scale(0.9);transform:scale(0.9)}.data-grid-search-control-wrap .action-submit:hover:before{color:#1a1a1a}._keyfocus .data-grid-search-control-wrap .action-submit:focus{box-shadow:0 0 0 1px #008bdb}.data-grid-search-control-wrap .action-submit:before{content:'\e60c';font-size:2rem;transition:color .1s linear}.data-grid-search-control-wrap .action-submit>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.data-grid-search-control-wrap .abs-action-menu .action-submenu,.data-grid-search-control-wrap .abs-action-menu .action-submenu .action-submenu,.data-grid-search-control-wrap .action-menu,.data-grid-search-control-wrap .action-menu .action-submenu,.data-grid-search-control-wrap .actions-split .action-menu .action-submenu,.data-grid-search-control-wrap .actions-split .action-menu .action-submenu .action-submenu,.data-grid-search-control-wrap .actions-split .dropdown-menu .action-submenu,.data-grid-search-control-wrap .actions-split .dropdown-menu .action-submenu .action-submenu{max-height:19.25rem;overflow-y:auto;z-index:398}.data-grid-search-control-wrap .action-menu-item._selected{background-color:#e0f6fe}.data-grid-search-control-wrap .data-grid-search-label{display:none}.data-grid-search-control{padding-right:6rem;width:100%}.data-grid-filters-action-wrap{float:left;padding-left:2rem}.data-grid-filters-action-wrap .action-default{font-size:1.3rem;margin-bottom:1rem;padding-left:1.7rem;padding-right:2.1rem;padding-top:.7rem}.data-grid-filters-action-wrap .action-default._active{background-color:#fff;border-bottom-color:#fff;border-right-color:#ccc;font-weight:600;margin:-.1rem 0 0;padding-bottom:1.6rem;padding-top:.8rem;position:relative;z-index:281}.data-grid-filters-action-wrap .action-default._active:after{background-color:#eb5202;bottom:100%;content:'';height:3px;left:-1px;position:absolute;right:-1px}.data-grid-filters-action-wrap .action-default:before{color:#333;content:'\e605';font-size:1.8rem;margin-right:.4rem;position:relative;top:-1px;vertical-align:top}.data-grid-filters-action-wrap .filters-active{display:none}.admin__action-grid-select .admin__control-select{margin:-.5rem .5rem 0 0;padding-bottom:.6rem;padding-top:.6rem}.admin__data-grid-filters-wrap{opacity:0;visibility:hidden;clear:both;font-size:1.3rem;transition:opacity .3s ease}.admin__data-grid-filters-wrap._show{opacity:1;visibility:visible;border-bottom:1px solid #ccc;border-top:1px solid #ccc;margin-bottom:.7rem;padding:3.6rem 0 3rem;position:relative;top:-1px;z-index:280}.admin__data-grid-filters-wrap._show .admin__data-grid-filters,.admin__data-grid-filters-wrap._show .admin__data-grid-filters-footer{display:block}.admin__data-grid-filters-wrap .admin__form-field-label,.admin__data-grid-filters-wrap .admin__form-field-legend{display:block;font-weight:700;margin:0 0 .3rem;text-align:left}.admin__data-grid-filters-wrap .admin__form-field{display:inline-block;margin-bottom:2em;margin-left:0;padding-left:2rem;padding-right:2rem;vertical-align:top;width:calc(100% / 4 - 4px)}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field{display:block;float:none;margin-bottom:1.5rem;padding-left:0;padding-right:0;width:auto}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field:last-child{margin-bottom:0}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field .admin__form-field-label{border:1px solid transparent;float:left;font-weight:400;line-height:1.36;margin-bottom:0;padding-bottom:.6rem;padding-right:1em;padding-top:.6rem;width:25%}.admin__data-grid-filters-wrap .admin__form-field .admin__form-field .admin__form-field-control{margin-left:25%}.admin__data-grid-filters-wrap .admin__action-multiselect,.admin__data-grid-filters-wrap .admin__control-select,.admin__data-grid-filters-wrap .admin__control-text,.admin__data-grid-filters-wrap .admin__form-field-label{font-size:1.3rem}.admin__data-grid-filters-wrap .admin__control-select{height:3.2rem;padding-top:.5rem}.admin__data-grid-filters-wrap .admin__action-multiselect:before{height:3.2rem;width:3.2rem}.admin__data-grid-filters-wrap .admin__control-select,.admin__data-grid-filters-wrap .admin__control-text._has-datepicker{width:100%}.admin__data-grid-filters{display:none;margin-left:-2rem;margin-right:-2rem}.admin__filters-legend{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.admin__data-grid-filters-footer{display:none;font-size:1.4rem}.admin__data-grid-filters-footer .admin__footer-main-actions{margin-left:25%;text-align:right}.admin__data-grid-filters-footer .admin__footer-secondary-actions{float:left;width:50%}.admin__data-grid-filters-current{border-bottom:.1rem solid #ccc;border-top:.1rem solid #ccc;display:none;font-size:1.3rem;margin-bottom:.9rem;padding-bottom:.8rem;padding-top:1.1rem;width:100%}.admin__data-grid-filters-current._show{display:table;position:relative;top:-1px;z-index:3}.admin__data-grid-filters-current._show+.admin__data-grid-filters-wrap._show{margin-top:-1rem}.admin__current-filters-actions-wrap,.admin__current-filters-list-wrap,.admin__current-filters-title-wrap{display:table-cell;vertical-align:top}.admin__current-filters-title{margin-right:1em;white-space:nowrap}.admin__current-filters-list-wrap{width:100%}.admin__current-filters-list{margin-bottom:0}.admin__current-filters-list>li{display:inline-block;font-weight:600;margin:0 1rem .5rem;padding-right:2.6rem;position:relative}.admin__current-filters-list .action-remove{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;padding:0;line-height:1;position:absolute;right:0;top:1px}.admin__current-filters-list .action-remove:hover{background-color:transparent;border:none;box-shadow:none}.admin__current-filters-list .action-remove:hover:before{color:#949494}.admin__current-filters-list .action-remove:active{-ms-transform:scale(0.9);transform:scale(0.9)}.admin__current-filters-list .action-remove:before{color:#adadad;content:'\e620';font-size:1.6rem;transition:color .1s linear}.admin__current-filters-list .action-remove>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.admin__current-filters-actions-wrap .action-clear{border:none;padding-bottom:0;padding-top:0;white-space:nowrap}.admin__data-grid-pager-wrap{float:right;text-align:right}.admin__data-grid-pager{display:inline-block;margin-left:3rem}.admin__data-grid-pager .admin__control-text::-webkit-inner-spin-button,.admin__data-grid-pager .admin__control-text::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.admin__data-grid-pager .admin__control-text{-moz-appearance:textfield;text-align:center;width:4.4rem}.action-next,.action-previous{width:4.4rem}.action-next:before,.action-previous:before{font-weight:700}.action-next>span,.action-previous>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.action-previous{margin-right:2.5rem;text-indent:-.25em}.action-previous:before{content:'\e629'}.action-next{margin-left:1.5rem;text-indent:.1em}.action-next:before{content:'\e62a'}.admin__data-grid-action-bookmarks{opacity:.98}.admin__data-grid-action-bookmarks .admin__action-dropdown-text:after{left:0;right:-6px}.admin__data-grid-action-bookmarks._active{z-index:290}.admin__data-grid-action-bookmarks .admin__action-dropdown .admin__action-dropdown-text{display:inline-block;max-width:15rem;min-width:4.9rem;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.admin__data-grid-action-bookmarks .admin__action-dropdown:before{content:'\e60f'}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu{font-size:1.3rem;left:0;padding:1rem 0;right:auto}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu>li{padding:0 5rem 0 0;position:relative;white-space:nowrap}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu>li:not(.action-dropdown-menu-action){transition:background-color .1s linear}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu>li:not(.action-dropdown-menu-action):hover{background-color:#e3e3e3}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item{max-width:23rem;min-width:18rem;white-space:normal;word-break:break-all}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-edit{display:none;padding-bottom:1rem;padding-left:1rem;padding-top:1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-edit .action-dropdown-menu-item-actions{padding-bottom:1rem;padding-top:1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action{padding-left:1rem;padding-top:1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action+.action-dropdown-menu-item-last{padding-top:.5rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action>a{color:#008bdb;text-decoration:none;display:inline-block;padding-left:1.1rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-action>a:hover{color:#0fa7ff;text-decoration:underline}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-last{padding-bottom:0}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu ._edit .action-dropdown-menu-item{display:none}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu ._edit .action-dropdown-menu-item-edit{display:block}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu ._active .action-dropdown-menu-link{font-weight:600}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .admin__control-text{font-size:1.3rem;min-width:15rem;width:calc(100% - 4rem)}.ie9 .admin__data-grid-action-bookmarks .admin__action-dropdown-menu .admin__control-text{width:15rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-item-actions{border-left:1px solid #fff;bottom:0;position:absolute;right:0;top:0;width:5rem}.admin__data-grid-action-bookmarks .admin__action-dropdown-menu .action-dropdown-menu-link{color:#333;display:block;text-decoration:none;padding:1rem 1rem 1rem 2.1rem}.admin__data-grid-action-bookmarks .action-delete,.admin__data-grid-action-bookmarks .action-edit,.admin__data-grid-action-bookmarks .action-submit{background-color:transparent;border:none;border-radius:0;box-shadow:none;margin:0;vertical-align:top}.admin__data-grid-action-bookmarks .action-delete:hover,.admin__data-grid-action-bookmarks .action-edit:hover,.admin__data-grid-action-bookmarks .action-submit:hover{background-color:transparent;border:none;box-shadow:none}.admin__data-grid-action-bookmarks .action-delete:before,.admin__data-grid-action-bookmarks .action-edit:before,.admin__data-grid-action-bookmarks .action-submit:before{font-size:1.7rem}.admin__data-grid-action-bookmarks .action-delete>span,.admin__data-grid-action-bookmarks .action-edit>span,.admin__data-grid-action-bookmarks .action-submit>span{clip:rect(0,0,0,0);overflow:hidden;position:absolute}.admin__data-grid-action-bookmarks .action-delete,.admin__data-grid-action-bookmarks .action-edit{padding:.6rem 1.4rem}.admin__data-grid-action-bookmarks .action-delete:active,.admin__data-grid-action-bookmarks .action-edit:active{-ms-transform:scale(0.9);transform:scale(0.9)}.admin__data-grid-action-bookmarks .action-submit{padding:.6rem 1rem .6rem .8rem}.admin__data-grid-action-bookmarks .action-submit:active{position:relative;right:-1px}.admin__data-grid-action-bookmarks .action-submit:before{content:'\e625'}.admin__data-grid-action-bookmarks .action-delete:before{content:'\e630'}.admin__data-grid-action-bookmarks .action-edit{padding-top:.8rem}.admin__data-grid-action-bookmarks .action-edit:before{content:'\e631'}.admin__data-grid-action-columns._active{opacity:.98;z-index:290}.admin__data-grid-action-columns .admin__action-dropdown:before{content:'\e610';font-size:1.8rem;margin-right:.7rem;vertical-align:top}.admin__data-grid-action-columns-menu{color:#303030;font-size:1.3rem;overflow:hidden;padding:2.2rem 3.5rem 1rem;z-index:1}.admin__data-grid-action-columns-menu._overflow .admin__action-dropdown-menu-header{border-bottom:1px solid #d1d1d1}.admin__data-grid-action-columns-menu._overflow .admin__action-dropdown-menu-content{width:49.2rem}.admin__data-grid-action-columns-menu._overflow .admin__action-dropdown-menu-footer{border-top:1px solid #d1d1d1;padding-top:2.5rem}.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-content{max-height:22.85rem;overflow-y:auto;padding-top:1.5rem;position:relative;width:47.4rem}.admin__data-grid-action-columns-menu .admin__field-option{float:left;height:1.9rem;margin-bottom:1.5rem;padding:0 1rem 0 0;width:15.8rem}.admin__data-grid-action-columns-menu .admin__field-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-header{padding-bottom:1.5rem}.admin__data-grid-action-columns-menu .admin__action-dropdown-menu-footer{padding:1rem 0 2rem}.admin__data-grid-action-columns-menu .admin__action-dropdown-footer-main-actions{margin-left:25%;text-align:right}.admin__data-grid-action-columns-menu .admin__action-dropdown-footer-secondary-actions{float:left;margin-left:-1em}.admin__data-grid-action-export._active{opacity:.98;z-index:290}.admin__data-grid-action-export .admin__action-dropdown:before{content:'\e635';font-size:1.7rem;left:.3rem;margin-right:.7rem;vertical-align:top}.admin__data-grid-action-export-menu{padding-left:2rem;padding-right:2rem;padding-top:1rem}.admin__data-grid-action-export-menu .admin__action-dropdown-footer-main-actions{padding-bottom:2rem;padding-top:2.5rem;white-space:nowrap}.sticky-header{background-color:#f8f8f8;border-bottom:1px solid #e3e3e3;box-shadow:0 5px 5px 0 rgba(0,0,0,.25);left:8.8rem;margin-top:-1px;padding:.5rem 3rem 0;position:fixed;right:0;top:77px;z-index:398}.sticky-header .admin__data-grid-wrap{margin-bottom:0;overflow-x:visible;padding-bottom:0}.sticky-header .admin__data-grid-header-row{position:relative;text-align:right}.sticky-header .admin__data-grid-header-row:last-child{margin:0}.sticky-header .admin__data-grid-actions-wrap,.sticky-header .admin__data-grid-filters-wrap,.sticky-header .admin__data-grid-pager-wrap,.sticky-header .data-grid-filters-actions-wrap,.sticky-header .data-grid-search-control-wrap{display:inline-block;float:none;vertical-align:top}.sticky-header .action-select-wrap{float:left;margin-right:1.5rem;width:16.66666667%}.sticky-header .admin__control-support-text{float:left}.sticky-header .data-grid-search-control-wrap{margin:-.5rem 0 0 1.1rem;width:auto}.sticky-header .data-grid-search-control-wrap .data-grid-search-label{box-sizing:border-box;cursor:pointer;display:block;min-width:3.8rem;padding:1.2rem .6rem 1.7rem;position:relative;text-align:center}.sticky-header .data-grid-search-control-wrap .data-grid-search-label:before{color:#333;content:'\e60c';font-size:2rem;transition:color .1s linear}.sticky-header .data-grid-search-control-wrap .data-grid-search-label:hover:before{color:#000}.sticky-header .data-grid-search-control-wrap .data-grid-search-label span{display:none}.sticky-header .data-grid-filters-actions-wrap{margin:-.5rem 0 0 1.1rem;padding-left:0;position:relative}.sticky-header .data-grid-filters-actions-wrap .action-default{background-color:transparent;border:1px solid transparent;box-sizing:border-box;min-width:3.8rem;padding:1.2rem .6rem 1.7rem;text-align:center;transition:all .15s ease}.sticky-header .data-grid-filters-actions-wrap .action-default span{display:none}.sticky-header .data-grid-filters-actions-wrap .action-default:before{margin:0}.sticky-header .data-grid-filters-actions-wrap .action-default._active{background-color:#fff;border-color:#adadad #adadad #fff;box-shadow:1px 1px 5px rgba(0,0,0,.5);z-index:210}.sticky-header .data-grid-filters-actions-wrap .action-default._active:after{background-color:#fff;content:'';height:6px;left:-2px;position:absolute;right:-6px;top:100%}.sticky-header .data-grid-filters-action-wrap{padding:0}.sticky-header .admin__data-grid-filters-wrap{background-color:#fff;border:1px solid #adadad;box-shadow:0 5px 5px 0 rgba(0,0,0,.25);left:0;padding-left:3.5rem;padding-right:3.5rem;position:absolute;top:100%;width:100%;z-index:209}.sticky-header .admin__data-grid-filters-current+.admin__data-grid-filters-wrap._show{margin-top:-6px}.sticky-header .filters-active{background-color:#e04f00;border-radius:10px;color:#fff;display:block;font-size:1.4rem;font-weight:700;padding:.1rem .7rem;position:absolute;right:-7px;top:0;z-index:211}.sticky-header .filters-active:empty{padding-bottom:0;padding-top:0}.sticky-header .admin__data-grid-actions-wrap{margin:-.5rem 0 0 1.1rem;padding-right:.3rem}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown{background-color:transparent;box-sizing:border-box;min-width:3.8rem;padding-left:.6rem;padding-right:.6rem;text-align:center}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown .admin__action-dropdown-text{display:inline-block;max-width:0;min-width:0;overflow:hidden}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown:before{margin:0}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown-wrap{margin-right:1.1rem}.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown-wrap:after,.sticky-header .admin__data-grid-actions-wrap .admin__action-dropdown:after{display:none}.sticky-header .admin__data-grid-actions-wrap ._active .admin__action-dropdown{background-color:#fff}.sticky-header .admin__data-grid-action-bookmarks .admin__action-dropdown:before{position:relative;top:-3px}.sticky-header .admin__data-grid-filters-current{border-bottom:0;border-top:0;margin-bottom:0;padding-bottom:0;padding-top:0}.sticky-header .admin__data-grid-pager .admin__control-text,.sticky-header .admin__data-grid-pager-wrap .admin__control-support-text,.sticky-header .data-grid-search-control-wrap .action-submit,.sticky-header .data-grid-search-control-wrap .data-grid-search-control{display:none}.sticky-header .action-next{margin:0}.sticky-header .data-grid{margin-bottom:-1px}.data-grid-cap-left,.data-grid-cap-right{background-color:#f8f8f8;bottom:-2px;position:absolute;top:6rem;width:3rem;z-index:201}.data-grid-cap-left{left:0}.admin__data-grid-header{font-size:1.4rem}.admin__data-grid-header-row+.admin__data-grid-header-row{margin-top:1.1rem}.admin__data-grid-header-row:last-child{margin-bottom:0}.admin__data-grid-header-row .action-select-wrap{display:block}.admin__data-grid-header-row .action-select{width:100%}.admin__data-grid-actions-wrap{float:right;margin-left:1.1rem;margin-top:-.5rem;text-align:right}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap{position:relative;text-align:left;vertical-align:middle}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active+.admin__action-dropdown-wrap:after,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active:after,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._hide+.admin__action-dropdown-wrap:after,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap:first-child:after{display:none}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active .admin__action-dropdown,.admin__data-grid-actions-wrap .admin__action-dropdown-wrap._active .admin__action-dropdown-menu{border-color:#adadad}.admin__data-grid-actions-wrap .admin__action-dropdown-wrap:after{border-left:1px solid #ccc;content:'';height:3.2rem;left:0;position:absolute;top:.5rem;z-index:3}.admin__data-grid-actions-wrap .admin__action-dropdown{padding-bottom:1.7rem;padding-top:1.2rem}.admin__data-grid-actions-wrap .admin__action-dropdown:after{margin-top:-.4rem}.admin__data-grid-outer-wrap{min-height:8rem;position:relative}.admin__data-grid-wrap{margin-bottom:2rem;max-width:100%;overflow-x:auto;padding-bottom:1rem;padding-top:2rem}.admin__data-grid-loading-mask{background:rgba(255,255,255,.5);bottom:0;left:0;position:absolute;right:0;top:0;z-index:399}.admin__data-grid-loading-mask .spinner{font-size:4rem;left:50%;margin-left:-2rem;margin-top:-2rem;position:absolute;top:50%}.ie9 .admin__data-grid-loading-mask .spinner{background:url(../images/loader-2.gif) 50% 50% no-repeat;bottom:0;height:149px;left:0;margin:auto;position:absolute;right:0;top:0;width:218px}.data-grid-cell-content{display:inline-block;overflow:hidden;width:100%}body._in-resize{cursor:col-resize;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body._in-resize *,body._in-resize .data-grid-th,body._in-resize .data-grid-th._draggable,body._in-resize .data-grid-th._sortable{cursor:col-resize!important}._layout-fixed{table-layout:fixed}.data-grid{border:none;font-size:1.3rem;margin-bottom:0;width:100%}.data-grid:not(._dragging-copy) ._odd-row td._dragging{background-color:#d0d0d0}.data-grid:not(._dragging-copy) ._dragging{background-color:#d9d9d9;color:rgba(48,48,48,.95)}.data-grid:not(._dragging-copy) ._dragging a{color:rgba(0,139,219,.95)}.data-grid:not(._dragging-copy) ._dragging a:hover{color:rgba(15,167,255,.95)}.data-grid._dragged{outline:#007bdb solid 1px}.data-grid thead{background-color:transparent}.data-grid tfoot th{padding:1rem}.data-grid tr._odd-row td{background-color:#f5f5f5}.data-grid tr._odd-row td._update-status-active{background:#89e1ff}.data-grid tr._odd-row td._update-status-upcoming{background:#b7ee63}.data-grid tr:hover td._update-status-active,.data-grid tr:hover td._update-status-upcoming{background-color:#e5f7fe}.data-grid tr.data-grid-tr-no-data td{font-size:1.6rem;padding:3rem;text-align:center}.data-grid tr.data-grid-tr-no-data:hover td{background-color:#fff;cursor:default}.data-grid tr:active td{background-color:#e0f6fe}.data-grid tr:hover td{background-color:#e5f7fe}.data-grid tr._dragged td{background:#d0d0d0}.data-grid tr._dragover-top td{box-shadow:inset 0 3px 0 0 #008bdb}.data-grid tr._dragover-bottom td{box-shadow:inset 0 -3px 0 0 #008bdb}.data-grid tr:not(.data-grid-editable-row):last-child td{border-bottom:.1rem solid #d6d6d6}.data-grid tr ._clickable,.data-grid tr._clickable{cursor:pointer}.data-grid tr._disabled{pointer-events:none}.data-grid td,.data-grid th{font-size:1.3rem;line-height:1.36;transition:background-color .1s linear;vertical-align:top}.data-grid td._resizing,.data-grid th._resizing{border-left:1px solid #007bdb;border-right:1px solid #007bdb}.data-grid td._hidden,.data-grid th._hidden{display:none}.data-grid td._fit,.data-grid th._fit{width:1%}.data-grid td{background-color:#fff;border-left:.1rem dashed #d6d6d6;border-right:.1rem dashed #d6d6d6;color:#303030;padding:1rem}.data-grid td:first-child{border-left-style:solid}.data-grid td:last-child{border-right-style:solid}.data-grid td .action-select-wrap{position:static}.data-grid td .action-select{color:#008bdb;text-decoration:none;background-color:transparent;border:none;font-size:1.3rem;padding:0 3rem 0 0;position:relative}.data-grid td .action-select:hover{color:#0fa7ff;text-decoration:underline}.data-grid td .action-select:hover:after{border-color:#0fa7ff transparent transparent}.data-grid td .action-select:after{border-color:#008bdb transparent transparent;margin:.6rem 0 0 .7rem;right:auto;top:auto}.data-grid td .action-select:before{display:none}.data-grid td .abs-action-menu .action-submenu,.data-grid td .abs-action-menu .action-submenu .action-submenu,.data-grid td .action-menu,.data-grid td .action-menu .action-submenu,.data-grid td .actions-split .action-menu .action-submenu,.data-grid td .actions-split .action-menu .action-submenu .action-submenu,.data-grid td .actions-split .dropdown-menu .action-submenu,.data-grid td .actions-split .dropdown-menu .action-submenu .action-submenu{left:auto;min-width:10rem;right:0;text-align:left;top:auto;z-index:1}.data-grid td._update-status-active{background:#bceeff}.data-grid td._update-status-upcoming{background:#ccf391}.data-grid th{background-color:#514943;border:.1rem solid #8a837f;border-left-color:transparent;color:#fff;font-weight:600;padding:0;text-align:left}.data-grid th:first-child{border-left-color:#8a837f}.data-grid th._dragover-left{box-shadow:inset 3px 0 0 0 #fff;z-index:2}.data-grid th._dragover-right{box-shadow:inset -3px 0 0 0 #fff}.data-grid .shadow-div{cursor:col-resize;height:100%;margin-right:-5px;position:absolute;right:0;top:0;width:10px}.data-grid .data-grid-th{background-clip:padding-box;color:#fff;padding:1rem;position:relative;vertical-align:middle}.data-grid .data-grid-th._resize-visible .shadow-div{cursor:auto;display:none}.data-grid .data-grid-th._draggable{cursor:grab}.data-grid .data-grid-th._sortable{cursor:pointer;transition:background-color .1s linear;z-index:1}.data-grid .data-grid-th._sortable:focus,.data-grid .data-grid-th._sortable:hover{background-color:#5f564f}.data-grid .data-grid-th._sortable:active{padding-bottom:.9rem;padding-top:1.1rem}.data-grid .data-grid-th.required>span:after{color:#f38a5e;content:'*';margin-left:.3rem}.data-grid .data-grid-checkbox-cell{overflow:hidden;padding:0;vertical-align:top;width:5.2rem}.data-grid .data-grid-checkbox-cell:hover{cursor:default}.data-grid .data-grid-thumbnail-cell{text-align:center;width:7rem}.data-grid .data-grid-thumbnail-cell img{border:1px solid #d6d6d6;width:5rem}.data-grid .data-grid-multicheck-cell{padding:1rem 1rem .9rem;text-align:center;vertical-align:middle}.data-grid .data-grid-onoff-cell{text-align:center;width:12rem}.data-grid .data-grid-actions-cell{padding-left:2rem;padding-right:2rem;text-align:center;width:1%}.data-grid._hidden{display:none}.data-grid._dragging-copy{box-shadow:1px 1px 5px rgba(0,0,0,.5);left:0;opacity:.95;position:fixed;top:0;z-index:1000}.data-grid._dragging-copy .data-grid-th{border:1px solid #007bdb;border-bottom:none}.data-grid._dragging-copy .data-grid-th,.data-grid._dragging-copy .data-grid-th._sortable{cursor:grabbing}.data-grid._dragging-copy tr:last-child td{border-bottom:1px solid #007bdb}.data-grid._dragging-copy td{border-left:1px solid #007bdb;border-right:1px solid #007bdb}.data-grid._dragging-copy._in-edit .data-grid-editable-row.data-grid-bulk-edit-panel td,.data-grid._dragging-copy._in-edit .data-grid-editable-row.data-grid-bulk-edit-panel td:before,.data-grid._dragging-copy._in-edit .data-grid-editable-row.data-grid-bulk-edit-panel:hover td{background-color:rgba(255,251,230,.95)}.data-grid._dragging-copy._in-edit .data-grid-editable-row td,.data-grid._dragging-copy._in-edit .data-grid-editable-row:hover td{background-color:rgba(255,255,255,.95)}.data-grid._dragging-copy._in-edit .data-grid-editable-row td:after,.data-grid._dragging-copy._in-edit .data-grid-editable-row td:before{left:0;right:0}.data-grid._dragging-copy._in-edit .data-grid-editable-row td:before{background-color:rgba(255,255,255,.95)}.data-grid._dragging-copy._in-edit .data-grid-editable-row td:only-child{border-left:1px solid #007bdb;border-right:1px solid #007bdb;left:0}.data-grid._dragging-copy._in-edit .data-grid-editable-row .admin__control-select,.data-grid._dragging-copy._in-edit .data-grid-editable-row .admin__control-text{opacity:.5}.data-grid .data-grid-controls-row td{padding-top:1.6rem}.data-grid .data-grid-controls-row td.data-grid-checkbox-cell{padding-top:.6rem}.data-grid .data-grid-controls-row td [class*=admin__control-],.data-grid .data-grid-controls-row td button{margin-top:-1.7rem}.data-grid._in-edit tr:hover td{background-color:#e6e6e6}.data-grid._in-edit ._odd-row.data-grid-editable-row td,.data-grid._in-edit ._odd-row.data-grid-editable-row:hover td{background-color:#fff}.data-grid._in-edit ._odd-row td,.data-grid._in-edit ._odd-row:hover td{background-color:#dcdcdc}.data-grid._in-edit .data-grid-editable-row-actions td,.data-grid._in-edit .data-grid-editable-row-actions:hover td{background-color:#fff}.data-grid._in-edit td{background-color:#e6e6e6;pointer-events:none}.data-grid._in-edit .data-grid-checkbox-cell{pointer-events:auto}.data-grid._in-edit .data-grid-editable-row{border:.1rem solid #adadad;border-bottom-color:#c2c2c2}.data-grid._in-edit .data-grid-editable-row:hover td{background-color:#fff}.data-grid._in-edit .data-grid-editable-row td{background-color:#fff;border-bottom-color:#fff;border-left-style:hidden;border-right-style:hidden;border-top-color:#fff;pointer-events:auto;vertical-align:middle}.data-grid._in-edit .data-grid-editable-row td:first-child{border-left-color:#adadad;border-left-style:solid}.data-grid._in-edit .data-grid-editable-row td:first-child:after,.data-grid._in-edit .data-grid-editable-row td:first-child:before{left:0}.data-grid._in-edit .data-grid-editable-row td:last-child{border-right-color:#adadad;border-right-style:solid;left:-.1rem}.data-grid._in-edit .data-grid-editable-row td:last-child:after,.data-grid._in-edit .data-grid-editable-row td:last-child:before{right:0}.data-grid._in-edit .data-grid-editable-row .admin__control-select,.data-grid._in-edit .data-grid-editable-row .admin__control-text{width:100%}.data-grid._in-edit .data-grid-bulk-edit-panel td{vertical-align:bottom}.data-grid .data-grid-editable-row td{border-left-color:#fff;border-left-style:solid;position:relative;z-index:1}.data-grid .data-grid-editable-row td:after{bottom:0;box-shadow:0 5px 5px rgba(0,0,0,.25);content:'';height:.9rem;left:0;margin-top:-1rem;position:absolute;right:0}.data-grid .data-grid-editable-row td:before{background-color:#fff;bottom:0;content:'';height:1rem;left:-10px;position:absolute;right:-10px;z-index:1}.data-grid .data-grid-editable-row.data-grid-editable-row-actions td,.data-grid .data-grid-editable-row.data-grid-editable-row-actions:hover td{background-color:#fff}.data-grid .data-grid-editable-row.data-grid-editable-row-actions td:first-child{border-left-color:#fff;border-right-color:#fff}.data-grid .data-grid-editable-row.data-grid-editable-row-actions td:last-child{left:0}.data-grid .data-grid-editable-row.data-grid-bulk-edit-panel td,.data-grid .data-grid-editable-row.data-grid-bulk-edit-panel td:before,.data-grid .data-grid-editable-row.data-grid-bulk-edit-panel:hover td{background-color:#fffbe6}.data-grid .data-grid-editable-row-actions{left:50%;margin-left:-12.5rem;margin-top:-2px;position:absolute;text-align:center}.data-grid .data-grid-editable-row-actions td{width:25rem}.data-grid .data-grid-editable-row-actions [class*=action-]{min-width:9rem}.data-grid .data-grid-draggable-row-cell{width:1%}.data-grid .data-grid-draggable-row-cell .draggable-handle{padding:0}.data-grid-th._sortable._ascend,.data-grid-th._sortable._descend{padding-right:2.7rem}.data-grid-th._sortable._ascend:before,.data-grid-th._sortable._descend:before{margin-top:-1em;position:absolute;right:1rem;top:50%}.data-grid-th._sortable._ascend:before{content:'\2193'}.data-grid-th._sortable._descend:before{content:'\2191'}.data-grid-checkbox-cell-inner{display:block;padding:1.1rem 1.8rem .9rem;text-align:right}.data-grid-checkbox-cell-inner:hover{cursor:pointer}.data-grid-state-cell-inner{display:block;padding:1.1rem 1.8rem .9rem;text-align:center}.data-grid-state-cell-inner>span{display:inline-block;font-style:italic;padding:.6rem 0}.data-grid-row-parent._active>td .data-grid-checkbox-cell-inner:before{content:'\e62b'}.data-grid-row-parent>td .data-grid-checkbox-cell-inner{padding-left:3.7rem;position:relative}.data-grid-row-parent>td .data-grid-checkbox-cell-inner:before{content:'\e628';font-size:1rem;font-weight:700;left:1.35rem;position:absolute;top:1.6rem}.data-grid-th._col-xs{width:1%}.data-grid-info-panel{box-shadow:0 0 5px rgba(0,0,0,.5);margin:2rem .1rem -2rem}.data-grid-info-panel .messages{overflow:hidden}.data-grid-info-panel .messages .message{margin:1rem}.data-grid-info-panel .messages .message:last-child{margin-bottom:1rem}.data-grid-info-panel-actions{padding:1rem;text-align:right}.data-grid-editable-row .admin__field-control{position:relative}.data-grid-editable-row .admin__field-control._error:after{border-color:transparent #ee7d7d transparent transparent;border-style:solid;border-width:0 12px 12px 0;content:'';position:absolute;right:0;top:0}.data-grid-editable-row .admin__field-control._error .admin__control-text{border-color:#ee7d7d}.data-grid-editable-row .admin__field-control._focus:after{display:none}.data-grid-editable-row .admin__field-error{bottom:100%;box-shadow:1px 1px 5px rgba(0,0,0,.5);left:0;margin:0 auto 1.5rem;max-width:32rem;position:absolute;right:0}.data-grid-editable-row .admin__field-error:after,.data-grid-editable-row .admin__field-error:before{border-style:solid;content:'';left:50%;position:absolute;top:100%}.data-grid-editable-row .admin__field-error:after{border-color:#fffbbb transparent transparent;border-width:10px 10px 0;margin-left:-10px;z-index:1}.data-grid-editable-row .admin__field-error:before{border-color:#ee7d7d transparent transparent;border-width:11px 12px 0;margin-left:-12px}.data-grid-bulk-edit-panel .admin__field-label-vertical{display:block;font-size:1.2rem;margin-bottom:.5rem;text-align:left}.data-grid-row-changed{cursor:default;display:block;opacity:.5;position:relative;width:100%;z-index:1}.data-grid-row-changed:after{content:'\e631';display:inline-block}.data-grid-row-changed .data-grid-row-changed-tooltip{background:#f1f1f1;border:1px solid #f1f1f1;border-radius:1px;bottom:100%;box-shadow:0 3px 9px 0 rgba(0,0,0,.3);display:none;font-weight:400;line-height:1.36;margin-bottom:1.5rem;padding:1rem;position:absolute;right:-1rem;text-transform:none;width:27rem;word-break:normal;z-index:2}.data-grid-row-changed._changed{opacity:1;z-index:3}.data-grid-row-changed._changed:hover .data-grid-row-changed-tooltip{display:block}.data-grid-row-changed._changed:hover:before{background:#f1f1f1;border:1px solid #f1f1f1;bottom:100%;box-shadow:4px 4px 3px -1px rgba(0,0,0,.15);content:'';display:block;height:1.6rem;left:50%;margin:0 0 .7rem -.8rem;position:absolute;-ms-transform:rotate(45deg);transform:rotate(45deg);width:1.6rem;z-index:3}.ie9 .data-grid-row-changed._changed:hover:before{display:none}.admin__data-grid-outer-wrap .data-grid-checkbox-cell{overflow:hidden}.admin__data-grid-outer-wrap .data-grid-checkbox-cell-inner{position:relative}.admin__data-grid-outer-wrap .data-grid-checkbox-cell-inner:before{bottom:0;content:'';height:500%;left:0;position:absolute;right:0;top:0}.admin__data-grid-wrap-static .data-grid-checkbox-cell:hover{cursor:pointer}.admin__data-grid-wrap-static .data-grid-checkbox-cell-inner{margin:1.1rem 1.8rem .9rem;padding:0}.adminhtml-cms-hierarchy-index .admin__data-grid-wrap-static .data-grid-actions-cell:first-child{padding:0}.adminhtml-export-index .admin__data-grid-wrap-static .data-grid-checkbox-cell-inner{margin:0;padding:1.1rem 1.8rem 1.9rem}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child:before,.admin__control-file-label:before,.admin__control-multiselect,.admin__control-select,.admin__control-text,.admin__control-textarea,.selectmenu{-webkit-appearance:none;background-color:#fff;border:1px solid #adadad;border-radius:1px;box-shadow:none;color:#303030;font-size:1.4rem;font-weight:400;height:auto;line-height:1.36;padding:.6rem 1rem;transition:border-color .1s linear;vertical-align:baseline;width:auto}.admin__control-addon [class*=admin__control-][class]:hover~[class*=admin__addon-]:last-child:before,.admin__control-multiselect:hover,.admin__control-select:hover,.admin__control-text:hover,.admin__control-textarea:hover,.selectmenu:hover,.selectmenu:hover .selectmenu-toggle:before{border-color:#878787}.admin__control-addon [class*=admin__control-][class]:focus~[class*=admin__addon-]:last-child:before,.admin__control-file:active+.admin__control-file-label:before,.admin__control-file:focus+.admin__control-file-label:before,.admin__control-multiselect:focus,.admin__control-select:focus,.admin__control-text:focus,.admin__control-textarea:focus,.selectmenu._focus,.selectmenu._focus .selectmenu-toggle:before{border-color:#007bdb;box-shadow:none;outline:0}.admin__control-addon [class*=admin__control-][class][disabled]~[class*=admin__addon-]:last-child:before,.admin__control-file[disabled]+.admin__control-file-label:before,.admin__control-multiselect[disabled],.admin__control-select[disabled],.admin__control-text[disabled],.admin__control-textarea[disabled]{background-color:#e9e9e9;border-color:#adadad;color:#303030;cursor:not-allowed;opacity:.5}.admin__field-row[class]>.admin__field-control,.admin__fieldset>.admin__field.admin__field-wide[class]>.admin__field-control{clear:left;float:none;text-align:left;width:auto}.admin__field-row[class]:not(.admin__field-option)>.admin__field-label,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)>.admin__field-label{display:block;line-height:1.4rem;margin-bottom:.86rem;margin-top:-.14rem;text-align:left;width:auto}.admin__field-row[class]:not(.admin__field-option)>.admin__field-label:before,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)>.admin__field-label:before{display:none}.admin__field-row[class]:not(.admin__field-option)._required>.admin__field-label span,.admin__field-row[class]:not(.admin__field-option).required>.admin__field-label span,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)._required>.admin__field-label span,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option).required>.admin__field-label span{padding-left:1.5rem}.admin__field-row[class]:not(.admin__field-option)._required>.admin__field-label span:after,.admin__field-row[class]:not(.admin__field-option).required>.admin__field-label span:after,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option)._required>.admin__field-label span:after,.admin__fieldset>.admin__field.admin__field-wide[class]:not(.admin__field-option).required>.admin__field-label span:after{left:0;margin-left:30px}.admin__legend{font-size:1.8rem;font-weight:600;margin-bottom:3rem}.admin__control-checkbox,.admin__control-radio{cursor:pointer;opacity:.01;overflow:hidden;position:absolute;vertical-align:top}.admin__control-checkbox:after,.admin__control-radio:after{display:none}.admin__control-checkbox+label,.admin__control-radio+label{cursor:pointer;display:inline-block}.admin__control-checkbox+label:before,.admin__control-radio+label:before{background-color:#fff;border:1px solid #adadad;color:transparent;float:left;height:1.6rem;text-align:center;vertical-align:top;width:1.6rem}.admin__control-checkbox+.admin__field-label,.admin__control-radio+.admin__field-label{padding-left:2.6rem}.admin__control-checkbox+.admin__field-label:before,.admin__control-radio+.admin__field-label:before{margin:1px 1rem 0 -2.6rem}.admin__control-checkbox:checked+label:before,.admin__control-radio:checked+label:before{color:#514943}.admin__control-checkbox.disabled+label,.admin__control-checkbox[disabled]+label,.admin__control-radio.disabled+label,.admin__control-radio[disabled]+label{color:#303030;cursor:default;opacity:.5}.admin__control-checkbox.disabled+label:before,.admin__control-checkbox[disabled]+label:before,.admin__control-radio.disabled+label:before,.admin__control-radio[disabled]+label:before{background-color:#e9e9e9;border-color:#adadad;cursor:default}._keyfocus .admin__control-checkbox:not(.disabled):focus+label:before,._keyfocus .admin__control-checkbox:not([disabled]):focus+label:before,._keyfocus .admin__control-radio:not(.disabled):focus+label:before,._keyfocus .admin__control-radio:not([disabled]):focus+label:before{border-color:#007bdb}.admin__control-checkbox:not(.disabled):hover+label:before,.admin__control-checkbox:not([disabled]):hover+label:before,.admin__control-radio:not(.disabled):hover+label:before,.admin__control-radio:not([disabled]):hover+label:before{border-color:#878787}.admin__control-radio+label:before{border-radius:1.6rem;content:'';transition:border-color .1s linear,color .1s ease-in}.admin__control-radio.admin__control-radio+label:before{line-height:140%}.admin__control-radio:checked+label{position:relative}.admin__control-radio:checked+label:after{background-color:#514943;border-radius:50%;content:'';height:10px;left:3px;position:absolute;top:4px;width:10px}.admin__control-radio:checked:not(.disabled):hover,.admin__control-radio:checked:not(.disabled):hover+label,.admin__control-radio:checked:not([disabled]):hover,.admin__control-radio:checked:not([disabled]):hover+label{cursor:default}.admin__control-radio:checked:not(.disabled):hover+label:before,.admin__control-radio:checked:not([disabled]):hover+label:before{border-color:#adadad}.admin__control-checkbox+label:before{border-radius:1px;content:'';font-size:0;transition:font-size .1s ease-out,color .1s ease-out,border-color .1s linear}.admin__control-checkbox:checked+label:before{content:'\e62d';font-size:1.1rem;line-height:125%}.admin__control-checkbox:not(:checked)._indeterminate+label:before,.admin__control-checkbox:not(:checked):indeterminate+label:before{color:#514943;content:'-';font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:700}input[type=checkbox].admin__control-checkbox,input[type=radio].admin__control-checkbox{margin:0;position:absolute}.admin__control-text{min-width:4rem}.admin__control-select{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;background-image:url(../images/arrows-bg.svg),linear-gradient(#e3e3e3,#e3e3e3),linear-gradient(#adadad,#adadad);background-position:calc(100% - 12px) -34px,100%,calc(100% - 3.2rem) 0;background-size:auto,3.2rem 100%,1px 100%;background-repeat:no-repeat;max-width:100%;min-width:8.5rem;padding-bottom:.5rem;padding-right:4.4rem;padding-top:.5rem;transition:border-color .1s linear}.admin__control-select:hover{border-color:#878787;cursor:pointer}.admin__control-select:focus{background-image:url(../images/arrows-bg.svg),linear-gradient(#e3e3e3,#e3e3e3),linear-gradient(#007bdb,#007bdb);background-position:calc(100% - 12px) 13px,100%,calc(100% - 3.2rem) 0;border-color:#007bdb}.admin__control-select::-ms-expand{display:none}.ie9 .admin__control-select{background-image:none;padding-right:1rem}option:empty{display:none}.admin__control-multiselect{height:auto;max-width:100%;min-width:15rem;overflow:auto;padding:0;resize:both}.admin__control-multiselect optgroup,.admin__control-multiselect option{padding:.5rem 1rem}.admin__control-file-wrapper{display:inline-block;padding:.5rem 1rem;position:relative;z-index:1}.admin__control-file-label:before{content:'';left:0;position:absolute;top:0;width:100%;z-index:0}.admin__control-file{background:0 0;border:0;padding-top:.7rem;position:relative;width:auto;z-index:1}.admin__control-support-text{border:1px solid transparent;display:inline-block;font-size:1.4rem;line-height:1.36;padding-bottom:.6rem;padding-top:.6rem}.admin__control-support-text+[class*=admin__control-],[class*=admin__control-]+.admin__control-support-text{margin-left:.7rem}.admin__control-service{float:left;margin:.8rem 0 0 3rem}.admin__control-textarea{height:8.48rem;line-height:1.18;padding-top:.8rem;resize:vertical}.admin__control-addon{-ms-flex-direction:row;flex-direction:row;display:inline-flex;-ms-flex-flow:row nowrap;flex-flow:row nowrap;position:relative;width:100%;z-index:1}.admin__control-addon>[class*=admin__addon-],.admin__control-addon>[class*=admin__control-]{-ms-flex-preferred-size:auto;flex-basis:auto;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-ms-flex-negative:0;flex-shrink:0;position:relative;z-index:1}.admin__control-addon .admin__control-select{width:auto}.admin__control-addon .admin__control-text{margin:.1rem;padding:.5rem .9rem;width:100%}.admin__control-addon [class*=admin__control-][class]{appearence:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-order:1;order:1;-ms-flex-negative:1;flex-shrink:1;background-color:transparent;border-color:transparent;box-shadow:none;vertical-align:top}.admin__control-addon [class*=admin__control-][class]+[class*=admin__control-]{border-left-color:#adadad}.admin__control-addon [class*=admin__control-][class] :focus{box-shadow:0}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child{padding-left:1rem;position:static!important;z-index:0}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child>*{position:relative;vertical-align:top;z-index:1}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child:empty{padding:0}.admin__control-addon [class*=admin__control-][class]~[class*=admin__addon-]:last-child:before{bottom:0;box-sizing:border-box;content:'';left:0;position:absolute;top:0;width:100%;z-index:-1}.admin__addon-prefix,.admin__addon-suffix{border:0;box-sizing:border-box;color:#858585;display:inline-block;font-size:1.4rem;font-weight:400;height:3.2rem;line-height:3.2rem;padding:0}.admin__addon-suffix{-ms-flex-order:3;order:3}.admin__addon-suffix:last-child{padding-right:1rem}.admin__addon-prefix{-ms-flex-order:0;order:0}.ie9 .admin__control-addon:after{clear:both;content:'';display:block;height:0;overflow:hidden}.ie9 .admin__addon{min-width:0;overflow:hidden;text-align:right;white-space:nowrap;width:auto}.ie9 .admin__addon [class*=admin__control-]{display:inline}.ie9 .admin__addon-prefix{float:left}.ie9 .admin__addon-suffix{float:right}.admin__control-collapsible{width:100%}.admin__control-collapsible ._dragged .admin__collapsible-block-wrapper .admin__collapsible-title{background:#d0d0d0}.admin__control-collapsible ._dragover-bottom .admin__collapsible-block-wrapper:before,.admin__control-collapsible ._dragover-top .admin__collapsible-block-wrapper:before{background:#008bdb;content:'';display:block;height:3px;left:0;position:absolute;right:0}.admin__control-collapsible ._dragover-top .admin__collapsible-block-wrapper:before{top:-3px}.admin__control-collapsible ._dragover-bottom .admin__collapsible-block-wrapper:before{bottom:-3px}.admin__control-collapsible .admin__collapsible-block-wrapper.fieldset-wrapper{border:0;margin:0;position:relative}.admin__control-collapsible .admin__collapsible-block-wrapper.fieldset-wrapper .fieldset-wrapper-title{background:#f8f8f8;border:2px solid #ccc}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .admin__collapsible-title{font-size:1.4rem;font-weight:400;line-height:1;padding:1.6rem 4rem 1.6rem 3.8rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .admin__collapsible-title:before{left:1rem;right:auto;top:1.4rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete{background-color:transparent;border-color:transparent;box-shadow:none;padding:0;position:absolute;right:1rem;top:1.4rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:hover{background-color:transparent;border-color:transparent;box-shadow:none}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete:before{content:'\e630';font-size:2rem}.admin__control-collapsible .admin__collapsible-block-wrapper .fieldset-wrapper-title .action-delete>span{display:none}.admin__control-collapsible .admin__collapsible-content{background-color:#fff;margin-bottom:1rem}.admin__control-collapsible .admin__collapsible-content>.fieldset-wrapper{border:1px solid #ccc;margin-top:-1px;padding:1rem}.admin__control-collapsible .admin__collapsible-content .admin__fieldset{padding:0}.admin__control-collapsible .admin__collapsible-content .admin__field:last-child{margin-bottom:0}.admin__control-table-wrapper{max-width:100%;overflow-x:auto;overflow-y:hidden}.admin__control-table{width:100%}.admin__control-table thead{background-color:transparent}.admin__control-table tbody td{vertical-align:top}.admin__control-table tfoot th{padding-bottom:1.3rem}.admin__control-table tfoot th.validation{padding-bottom:0;padding-top:0}.admin__control-table tfoot td{border-top:1px solid #fff}.admin__control-table tfoot .admin__control-table-pagination{float:right;padding-bottom:0}.admin__control-table tfoot .action-previous{margin-right:.5rem}.admin__control-table tfoot .action-next{margin-left:.9rem}.admin__control-table tr:last-child td{border-bottom:none}.admin__control-table tr._dragover-top td{box-shadow:inset 0 3px 0 0 #008bdb}.admin__control-table tr._dragover-bottom td{box-shadow:inset 0 -3px 0 0 #008bdb}.admin__control-table tr._dragged td,.admin__control-table tr._dragged th{background:#d0d0d0}.admin__control-table td,.admin__control-table th{background-color:#efefef;border:0;border-bottom:1px solid #fff;padding:1.3rem 1rem 1.3rem 0;text-align:left;vertical-align:top}.admin__control-table td:first-child,.admin__control-table th:first-child{padding-left:1rem}.admin__control-table td>.admin__control-select,.admin__control-table td>.admin__control-text,.admin__control-table th>.admin__control-select,.admin__control-table th>.admin__control-text{width:100%}.admin__control-table td._hidden,.admin__control-table th._hidden{display:none}.admin__control-table td._fit,.admin__control-table th._fit{width:1px}.admin__control-table th{color:#303030;font-size:1.4rem;font-weight:600;vertical-align:bottom}.admin__control-table th._required span:after{color:#eb5202;content:'*'}.admin__control-table .control-table-actions-th{white-space:nowrap}.admin__control-table .control-table-actions-cell{padding-top:1.8rem;text-align:center;width:1%}.admin__control-table .control-table-options-th{text-align:center;width:10rem}.admin__control-table .control-table-options-cell{text-align:center}.admin__control-table .control-table-text{line-height:3.2rem}.admin__control-table .col-draggable{padding-top:2.2rem;width:1%}.admin__control-table .action-delete{background-color:transparent;border-color:transparent;box-shadow:none;padding-left:0;padding-right:0}.admin__control-table .action-delete:hover{background-color:transparent;border-color:transparent;box-shadow:none}.admin__control-table .action-delete:before{content:'\e630';font-size:2rem}.admin__control-table .action-delete>span{display:none}.admin__control-table .draggable-handle{padding:0}.admin__control-table._dragged{outline:#007bdb solid 1px}.admin__control-table-action{background-color:#efefef;border-top:1px solid #fff;padding:1.3rem 1rem}.admin__dynamic-rows._dragged{opacity:.95;position:absolute;z-index:999}.admin__dynamic-rows.admin__control-table .admin__control-fields>.admin__field{border:0;padding:0}.admin__dynamic-rows td>.admin__field{border:0;margin:0;padding:0}.admin__control-table-pagination{padding-bottom:1rem}.admin__control-table-pagination .admin__data-grid-pager{float:right}.admin__field-tooltip{display:inline-block;margin-top:.5rem;max-width:45px;overflow:visible;vertical-align:top;width:0}.admin__field-tooltip:hover{position:relative;z-index:500}.admin__field-option .admin__field-tooltip{margin-top:.5rem}.admin__field-tooltip .admin__field-tooltip-action{margin-left:2rem;position:relative;z-index:2;display:inline-block;text-decoration:none}.admin__field-tooltip .admin__field-tooltip-action:before{-webkit-font-smoothing:antialiased;font-size:2.2rem;line-height:1;color:#514943;content:'\e633';font-family:Icons;vertical-align:middle;display:inline-block;font-weight:400;overflow:hidden;speak:none;text-align:center}.admin__field-tooltip .admin__control-text:focus+.admin__field-tooltip-content,.admin__field-tooltip:hover .admin__field-tooltip-content{display:block}.admin__field-tooltip .admin__field-tooltip-content{bottom:3.8rem;display:none;right:-2.3rem}.admin__field-tooltip .admin__field-tooltip-content:after,.admin__field-tooltip .admin__field-tooltip-content:before{border:1.6rem solid transparent;height:0;width:0;border-top-color:#afadac;content:'';display:block;position:absolute;right:2rem;top:100%;z-index:3}.admin__field-tooltip .admin__field-tooltip-content:after{border-top-color:#fffbbb;margin-top:-1px;z-index:4}.abs-admin__field-tooltip-content,.admin__field-tooltip .admin__field-tooltip-content{box-shadow:0 2px 8px 0 rgba(0,0,0,.3);background:#fffbbb;border:1px solid #afadac;border-radius:1px;padding:1.5rem 2.5rem;position:absolute;width:32rem;z-index:1}.admin__field-fallback-reset{font-size:1.25rem;white-space:nowrap;width:30px}.admin__field-fallback-reset>span{margin-left:.5rem;position:relative}.admin__field-fallback-reset:active{-ms-transform:scale(0.98);transform:scale(0.98)}.admin__field-fallback-reset:before{transition:color .1s linear;content:'\e642';font-size:1.3rem;margin-left:.5rem}.admin__field-fallback-reset:hover{cursor:pointer;text-decoration:none}.admin__field-fallback-reset:focus{background:0 0}.abs-field-size-x-small,.abs-field-sizes.admin__field-x-small>.admin__field-control,.admin__field.admin__field-x-small>.admin__field-control,.admin__fieldset>.admin__field.admin__field-x-small>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-x-small>.admin__field-control{width:8rem}.abs-field-size-small,.abs-field-sizes.admin__field-small>.admin__field-control,.admin__control-grouped-date>.admin__field-date.admin__field>.admin__field-control,.admin__field.admin__field-small>.admin__field-control,.admin__fieldset>.admin__field.admin__field-small>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-small>.admin__field-control{width:15rem}.abs-field-size-medium,.abs-field-sizes.admin__field-medium>.admin__field-control,.admin__field.admin__field-medium>.admin__field-control,.admin__fieldset>.admin__field.admin__field-medium>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-medium>.admin__field-control{width:34rem}.abs-field-size-large,.abs-field-sizes.admin__field-large>.admin__field-control,.admin__field.admin__field-large>.admin__field-control,.admin__fieldset>.admin__field.admin__field-large>.admin__field-control,[class*=admin__control-grouped]>.admin__field.admin__field-large>.admin__field-control{width:64rem}.abs-field-no-label,.admin__field-group-additional,.admin__field-no-label,.admin__fieldset>.admin__field.admin__field-no-label>.admin__field-control{margin-left:calc((100%) * .25 + 30px)}.admin__fieldset{border:0;margin:0;min-width:0;padding:0}.admin__fieldset .fieldset-wrapper.admin__fieldset-section>.fieldset-wrapper-title{padding-left:1rem}.admin__fieldset .fieldset-wrapper.admin__fieldset-section>.fieldset-wrapper-title strong{font-size:1.7rem;font-weight:600}.admin__fieldset .fieldset-wrapper.admin__fieldset-section .admin__fieldset-wrapper-content>.admin__fieldset{padding-top:1rem}.admin__fieldset .fieldset-wrapper.admin__fieldset-section:last-child .admin__fieldset-wrapper-content>.admin__fieldset{padding-bottom:0}.admin__fieldset>.admin__field{border:0;margin:0 0 0 -30px;padding:0}.admin__fieldset>.admin__field:after{clear:both;content:'';display:table}.admin__fieldset>.admin__field>.admin__field-control{width:calc((100%) * .5 - 30px);float:left;margin-left:30px}.admin__fieldset>.admin__field>.admin__field-label{width:calc((100%) * .25 - 30px);float:left;margin-left:30px}.admin__fieldset>.admin__field.admin__field-no-label>.admin__field-label{display:none}.admin__fieldset>.admin__field+.admin__field._empty._no-header{margin-top:-3rem}.admin__fieldset-product-websites{position:relative;z-index:300}.admin__fieldset-note{margin-bottom:2rem}.admin__form-field{border:0;margin:0;padding:0}.admin__field-control .admin__control-text,.admin__field-control .admin__control-textarea,.admin__form-field-control .admin__control-text,.admin__form-field-control .admin__control-textarea{width:100%}.admin__field-label{color:#303030;cursor:pointer;margin:0;text-align:right}.admin__field-label+br{display:none}.admin__field:not(.admin__field-option)>.admin__field-label{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.4rem;font-weight:600;line-height:3.2rem;padding:0;white-space:nowrap}.admin__field:not(.admin__field-option)>.admin__field-label:before{opacity:0;visibility:hidden;content:'.';margin-left:-7px;overflow:hidden}.admin__field:not(.admin__field-option)>.admin__field-label span{display:inline-block;line-height:1.2;vertical-align:middle;white-space:normal}.admin__field:not(.admin__field-option)>.admin__field-label span[data-config-scope]{position:relative}._required>.admin__field-label>span:after,.required>.admin__field-label>span:after{color:#eb5202;content:'*';display:inline-block;font-size:1.6rem;font-weight:500;line-height:1;margin-left:10px;margin-top:.2rem;position:absolute;z-index:1}._disabled>.admin__field-label{color:#999;cursor:default}.admin__field{margin-bottom:0}.admin__field+.admin__field{margin-top:1.5rem}.admin__field:not(.admin__field-option)~.admin__field-option{margin-top:.5rem}.admin__field.admin__field-option~.admin__field-option{margin-top:.9rem}.admin__field~.admin__field-option:last-child{margin-bottom:.8rem}.admin__fieldset>.admin__field{margin-bottom:3rem;position:relative}.admin__field legend.admin__field-label{opacity:0}.admin__field[data-config-scope]:before{color:gray;content:attr(data-config-scope);display:inline-block;font-size:1.2rem;left:calc((100%) * .75 - 30px);line-height:3.2rem;margin-left:60px;position:absolute;width:calc((100%) * .25 - 30px)}.admin__field-control .admin__field[data-config-scope]:nth-child(n+2):before{content:''}.admin__field._error .admin__field-control [class*=admin__addon-]:before,.admin__field._error .admin__field-control [class*=admin__control-] [class*=admin__addon-]:before,.admin__field._error .admin__field-control>[class*=admin__control-]{border-color:#e22626}.admin__field._disabled,.admin__field._disabled:hover{box-shadow:inherit;cursor:inherit;opacity:1;outline:inherit}.admin__field._hidden{display:none}.admin__field-control+.admin__field-control{margin-top:1.5rem}.admin__field-control._with-tooltip>.admin__control-addon,.admin__field-control._with-tooltip>.admin__control-select,.admin__field-control._with-tooltip>.admin__control-text,.admin__field-control._with-tooltip>.admin__control-textarea,.admin__field-control._with-tooltip>.admin__field-option{max-width:calc(100% - 45px - 4px)}.admin__field-control._with-tooltip .admin__field-tooltip{width:auto}.admin__field-control._with-tooltip .admin__field-option{display:inline-block}.admin__field-control._with-reset>.admin__control-addon,.admin__field-control._with-reset>.admin__control-text,.admin__field-control._with-reset>.admin__control-textarea{width:calc(100% - 30px - .5rem - 4px)}.admin__field-control._with-reset .admin__field-fallback-reset{margin-left:.5rem;margin-top:1rem;vertical-align:top}.admin__field-control._with-reset._with-tooltip>.admin__control-addon,.admin__field-control._with-reset._with-tooltip>.admin__control-text,.admin__field-control._with-reset._with-tooltip>.admin__control-textarea{width:calc(100% - 30px - .5rem - 45px - 8px)}.admin__fieldset>.admin__field-collapsible{margin-bottom:0}.admin__fieldset>.admin__field-collapsible .admin__field-control{border-top:1px solid #ccc;display:block;font-size:1.7rem;font-weight:700;padding:1.7rem 0;width:calc(97%)}.admin__fieldset>.admin__field-collapsible .admin__field-option{padding-top:0}.admin__field-collapsible+div{margin-top:2.5rem}.admin__field-collapsible .admin__control-radio+label:before{height:1.8rem;width:1.8rem}.admin__field-collapsible .admin__control-radio:checked+label:after{left:4px;top:5px}.admin__field-error{background:#fffbbb;border:1px solid #ee7d7d;box-sizing:border-box;color:#555;display:block;font-size:1.2rem;font-weight:400;line-height:1.2;margin:.2rem 0 0;padding:.8rem 1rem .9rem}.admin__field-note{color:#303030;font-size:1.2rem;margin:10px 0 0;padding:0}.admin__additional-info{padding-top:1rem}.admin__field-option{padding-top:.7rem}.admin__field-option .admin__field-label{text-align:left}.admin__field-control>.admin__field-option:nth-child(1):nth-last-child(2),.admin__field-control>.admin__field-option:nth-child(2):nth-last-child(1){display:inline-block}.admin__field-control>.admin__field-option:nth-child(1):nth-last-child(2)+.admin__field-option,.admin__field-control>.admin__field-option:nth-child(2):nth-last-child(1)+.admin__field-option{display:inline-block;margin-left:41px;margin-top:0}.admin__field-control>.admin__field-option:nth-child(1):nth-last-child(2)+.admin__field-option:before,.admin__field-control>.admin__field-option:nth-child(2):nth-last-child(1)+.admin__field-option:before{background:#cacaca;content:'';display:inline-block;height:20px;margin-left:-20px;position:absolute;width:1px}.admin__field-value{display:inline-block;padding-top:.7rem}.admin__field-service{padding-top:1rem}.admin__control-fields>.admin__field:first-child,[class*=admin__control-grouped]>.admin__field:first-child{position:static}.admin__control-fields>.admin__field:first-child>.admin__field-label,[class*=admin__control-grouped]>.admin__field:first-child>.admin__field-label{width:calc((100%) * .25 - 30px);float:left;margin-left:30px;background:#fff;cursor:pointer;left:0;position:absolute;top:0}.admin__control-fields>.admin__field:first-child>.admin__field-label span:before,[class*=admin__control-grouped]>.admin__field:first-child>.admin__field-label span:before{display:block}.admin__control-fields>.admin__field._disabled>.admin__field-label,[class*=admin__control-grouped]>.admin__field._disabled>.admin__field-label{cursor:default}.admin__control-fields>.admin__field>.admin__field-label span:before,[class*=admin__control-grouped]>.admin__field>.admin__field-label span:before{display:none}.admin__control-fields .admin__field-label~.admin__field-control{width:100%}.admin__control-fields .admin__field-option{padding-top:0}[class*=admin__control-grouped]{box-sizing:border-box;display:table;width:100%}[class*=admin__control-grouped]>.admin__field{display:table-cell;vertical-align:top}[class*=admin__control-grouped]>.admin__field>.admin__field-control{float:none;width:100%}[class*=admin__control-grouped]>.admin__field.admin__field-default,[class*=admin__control-grouped]>.admin__field.admin__field-large,[class*=admin__control-grouped]>.admin__field.admin__field-medium,[class*=admin__control-grouped]>.admin__field.admin__field-small,[class*=admin__control-grouped]>.admin__field.admin__field-x-small{width:1px}[class*=admin__control-grouped]>.admin__field.admin__field-default+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-large+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-medium+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-small+.admin__field:last-child,[class*=admin__control-grouped]>.admin__field.admin__field-x-small+.admin__field:last-child{width:auto}[class*=admin__control-grouped]>.admin__field:nth-child(n+2){padding-left:20px}.admin__control-group-equal{table-layout:fixed}.admin__control-group-equal>.admin__field{width:50%}.admin__field-control-group{margin-top:.8rem}.admin__field-control-group>.admin__field{padding:0}.admin__control-grouped-date>.admin__field-date{white-space:nowrap;width:1px}.admin__control-grouped-date>.admin__field-date.admin__field>.admin__field-control{float:left;position:relative}.admin__control-grouped-date>.admin__field-date+.admin__field:last-child{width:auto}.admin__control-grouped-date>.admin__field-date+.admin__field-date>.admin__field-label{float:left;padding-right:20px}.admin__control-grouped-date .ui-datepicker-trigger{left:100%;top:0}.admin__field-group-columns.admin__field-control.admin__control-grouped{width:calc((100%) * 1 - 30px);float:left;margin-left:30px}.admin__field-group-columns>.admin__field:first-child>.admin__field-label{float:none;margin:0;opacity:1;position:static;text-align:left}.admin__field-group-columns .admin__control-select{width:100%}.admin__field-group-additional{clear:both}.admin__field-group-additional .action-advanced{margin-top:1rem}.admin__field-group-additional .action-secondary{width:100%}.admin__field-group-show-label{white-space:nowrap}.admin__field-group-show-label>.admin__field-control,.admin__field-group-show-label>.admin__field-label{display:inline-block;vertical-align:top}.admin__field-group-show-label>.admin__field-label{margin-right:20px}.admin__field-complex{margin:1rem 0 3rem;padding-left:1rem}.admin__field:not(._hidden)+.admin__field-complex{margin-top:3rem}.admin__field-complex .admin__field-complex-title{clear:both;color:#303030;font-size:1.7rem;font-weight:600;letter-spacing:.025em;margin-bottom:1rem}.admin__field-complex .admin__field-complex-elements{float:right;max-width:40%}.admin__field-complex .admin__field-complex-elements button{margin-left:1rem}.admin__field-complex .admin__field-complex-content{max-width:60%;overflow:hidden}.admin__field-complex .admin__field-complex-text{margin-left:-1rem}.admin__field-complex+.admin__field._empty._no-header{margin-top:-3rem}.admin__legend{float:left;position:static;width:100%}.admin__legend+br{clear:left;display:block;height:0;overflow:hidden}.message{margin-bottom:3rem}.message-icon-top:before{margin-top:0;top:1.8rem}.nav{background-color:#f8f8f8;border-bottom:1px solid #e3e3e3;border-top:1px solid #e3e3e3;display:none;margin-bottom:3rem;padding:2.2rem 1.5rem 0 0}.nav .btn-group,.nav-bar-outer-actions{float:right;margin-bottom:1.7rem}.nav .btn-group .btn-wrap,.nav-bar-outer-actions .btn-wrap{float:right;margin-left:.5rem;margin-right:.5rem}.nav .btn-group .btn-wrap .btn,.nav-bar-outer-actions .btn-wrap .btn{padding-left:.5rem;padding-right:.5rem}.nav-bar-outer-actions{margin-top:-10.6rem;padding-right:1.5rem}.btn-wrap-try-again{width:9.5rem}.btn-wrap-next,.btn-wrap-prev{width:8.5rem}.nav-bar{counter-reset:i;float:left;margin:0 1rem 1.7rem 0;padding:0;position:relative;white-space:nowrap}.nav-bar:before{background-color:#d4d4d4;background-repeat:repeat-x;background-image:linear-gradient(to bottom,#d1d1d1 0,#d4d4d4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#d1d1d1', endColorstr='#d4d4d4', GradientType=0);border-bottom:1px solid #d9d9d9;border-top:1px solid #bfbfbf;content:'';height:1rem;left:5.15rem;position:absolute;right:5.15rem;top:.7rem}.nav-bar>li{display:inline-block;font-size:0;position:relative;vertical-align:top;width:10.3rem}.nav-bar>li:first-child:after{display:none}.nav-bar>li:after{background-color:#514943;content:'';height:.5rem;left:calc(-50% + .25rem);position:absolute;right:calc(50% + .7rem);top:.9rem}.nav-bar>li.disabled:before,.nav-bar>li.ui-state-disabled:before{bottom:0;content:'';left:0;position:absolute;right:0;top:0;z-index:1}.nav-bar>li.active~li:after,.nav-bar>li.ui-state-active~li:after{display:none}.nav-bar>li.active~li a:after,.nav-bar>li.ui-state-active~li a:after{background-color:transparent;border-color:transparent;color:#a6a6a6}.nav-bar>li.active a,.nav-bar>li.ui-state-active a{color:#000}.nav-bar>li.active a:hover,.nav-bar>li.ui-state-active a:hover{cursor:default}.nav-bar>li.active a:after,.nav-bar>li.ui-state-active a:after{background-color:#fff;content:''}.nav-bar a{color:#514943;display:block;font-size:1.2rem;font-weight:600;line-height:1.2;overflow:hidden;padding:3rem .5em 0;position:relative;text-align:center;text-overflow:ellipsis}.nav-bar a:hover{text-decoration:none}.nav-bar a:after{background-color:#514943;border:.4rem solid #514943;border-radius:100%;color:#fff;content:counter(i);counter-increment:i;height:1.5rem;left:50%;line-height:.6;margin-left:-.8rem;position:absolute;right:auto;text-align:center;top:.4rem;width:1.5rem}.nav-bar a:before{background-color:#d6d6d6;border:1px solid transparent;border-bottom-color:#d9d9d9;border-radius:100%;border-top-color:#bfbfbf;content:'';height:2.3rem;left:50%;line-height:1;margin-left:-1.2rem;position:absolute;top:0;width:2.3rem}.tooltip{display:block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.19rem;font-weight:400;line-height:1.4;opacity:0;position:absolute;visibility:visible;z-index:10}.tooltip.in{opacity:.9}.tooltip.top{margin-top:-4px;padding:8px 0}.tooltip.right{margin-left:4px;padding:0 8px}.tooltip.bottom{margin-top:4px;padding:8px 0}.tooltip.left{margin-left:-4px;padding:0 8px}.tooltip p:last-child{margin-bottom:0}.tooltip-inner{background-color:#fff;border:1px solid #adadad;border-radius:0;box-shadow:1px 1px 1px #ccc;color:#41362f;max-width:31rem;padding:.5em 1em;text-decoration:none}.tooltip-arrow,.tooltip-arrow:after{border:solid transparent;height:0;position:absolute;width:0}.tooltip-arrow:after{content:'';position:absolute}.tooltip.top .tooltip-arrow,.tooltip.top .tooltip-arrow:after{border-top-color:#949494;border-width:8px 8px 0;bottom:0;left:50%;margin-left:-8px}.tooltip.top-left .tooltip-arrow,.tooltip.top-left .tooltip-arrow:after{border-top-color:#949494;border-width:8px 8px 0;bottom:0;margin-bottom:-8px;right:8px}.tooltip.top-right .tooltip-arrow,.tooltip.top-right .tooltip-arrow:after{border-top-color:#949494;border-width:8px 8px 0;bottom:0;left:8px;margin-bottom:-8px}.tooltip.right .tooltip-arrow,.tooltip.right .tooltip-arrow:after{border-right-color:#949494;border-width:8px 8px 8px 0;left:1px;margin-top:-8px;top:50%}.tooltip.right .tooltip-arrow:after{border-right-color:#fff;border-width:6px 7px 6px 0;margin-left:0;margin-top:-6px}.tooltip.left .tooltip-arrow,.tooltip.left .tooltip-arrow:after{border-left-color:#949494;border-width:8px 0 8px 8px;margin-top:-8px;right:0;top:50%}.tooltip.bottom .tooltip-arrow,.tooltip.bottom .tooltip-arrow:after{border-bottom-color:#949494;border-width:0 8px 8px;left:50%;margin-left:-8px;top:0}.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow:after{border-bottom-color:#949494;border-width:0 8px 8px;margin-top:-8px;right:8px;top:0}.tooltip.bottom-right .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow:after{border-bottom-color:#949494;border-width:0 8px 8px;left:8px;margin-top:-8px;top:0}.password-strength{display:block;margin:0 -.3rem 1em;white-space:nowrap}.password-strength.password-strength-too-short .password-strength-item:first-child,.password-strength.password-strength-weak .password-strength-item:first-child,.password-strength.password-strength-weak .password-strength-item:first-child+.password-strength-item{background-color:#e22626}.password-strength.password-strength-fair .password-strength-item:first-child,.password-strength.password-strength-fair .password-strength-item:first-child+.password-strength-item,.password-strength.password-strength-fair .password-strength-item:first-child+.password-strength-item+.password-strength-item{background-color:#ef672f}.password-strength.password-strength-good .password-strength-item:first-child,.password-strength.password-strength-good .password-strength-item:first-child+.password-strength-item,.password-strength.password-strength-good .password-strength-item:first-child+.password-strength-item+.password-strength-item,.password-strength.password-strength-good .password-strength-item:first-child+.password-strength-item+.password-strength-item+.password-strength-item,.password-strength.password-strength-strong .password-strength-item{background-color:#79a22e}.password-strength .password-strength-item{background-color:#ccc;display:inline-block;font-size:0;height:1.4rem;margin-right:.3rem;width:calc(20% - .6rem)}@keyframes progress-bar-stripes{from{background-position:4rem 0}to{background-position:0 0}}.progress{background-color:#fafafa;border:1px solid #ccc;clear:left;height:3rem;margin-bottom:3rem;overflow:hidden}.progress-bar{background-color:#79a22e;color:#fff;float:left;font-size:1.19rem;height:100%;line-height:3rem;text-align:center;transition:width .6s ease;width:0}.progress-bar.active{animation:progress-bar-stripes 2s linear infinite}.progress-bar-text-description{margin-bottom:1.6rem}.progress-bar-text-progress{text-align:right}.page-columns .page-inner-sidebar{margin:0 0 3rem}.page-header{margin-bottom:2.7rem;padding-bottom:2rem;position:relative}.page-header:before{border-bottom:1px solid #e3e3e3;bottom:0;content:'';display:block;height:1px;left:3rem;position:absolute;right:3rem}.container .page-header:before{content:normal}.page-header .message{margin-bottom:1.8rem}.page-header .message+.message{margin-top:-1.5rem}.page-header .admin__action-dropdown,.page-header .search-global-input{transition:none}.container .page-header{margin-bottom:0}.page-title-wrapper{margin-top:1.1rem}.container .page-title-wrapper{background:url(../../pub/images/logo.svg) no-repeat;min-height:41px;padding:4px 0 0 45px}.admin__menu .level-0:first-child>a{margin-top:1.6rem}.admin__menu .level-0:first-child>a:after{top:-1.6rem}.admin__menu .level-0:first-child._active>a:after{display:block}.admin__menu .level-0>a{padding-bottom:1.3rem;padding-top:1.3rem}.admin__menu .level-0>a:before{margin-bottom:.7rem}.admin__menu .item-home>a:before{content:'\e611';font-size:2.3rem;padding-top:-.1rem}.admin__menu .item-component>a:before{content:'\e612'}.admin__menu .item-extension>a:before{content:'\e612'}.admin__menu .item-module>a:before{content:'\e647'}.admin__menu .item-upgrade>a:before{content:'\e614'}.admin__menu .item-system-config>a:before{content:'\e610'}.admin__menu .item-tools>a:before{content:'\e613'}.modal-sub-title{font-size:1.7rem;font-weight:600}.modal-connect-signin .modal-inner-wrap{max-width:80rem}@keyframes ngdialog-fadeout{0%{opacity:1}100%{opacity:0}}@keyframes ngdialog-fadein{0%{opacity:0}100%{opacity:1}}.ngdialog{-webkit-overflow-scrolling:touch;bottom:0;box-sizing:border-box;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:999}.ngdialog *,.ngdialog:after,.ngdialog:before{box-sizing:inherit}.ngdialog.ngdialog-disabled-animation *{animation:none!important}.ngdialog.ngdialog-closing .ngdialog-content,.ngdialog.ngdialog-closing .ngdialog-overlay{-webkit-animation:ngdialog-fadeout .5s;-webkit-backface-visibility:hidden;animation:ngdialog-fadeout .5s}.ngdialog-overlay{-webkit-animation:ngdialog-fadein .5s;-webkit-backface-visibility:hidden;animation:ngdialog-fadein .5s;background:rgba(0,0,0,.4);bottom:0;left:0;position:fixed;right:0;top:0}.ngdialog-content{-webkit-animation:ngdialog-fadein .5s;-webkit-backface-visibility:hidden;animation:ngdialog-fadein .5s}body.ngdialog-open{overflow:hidden}.component-indicator{border-radius:50%;cursor:help;display:inline-block;height:16px;text-align:center;vertical-align:middle;width:16px}.component-indicator::after,.component-indicator::before{background:#fff;display:block;opacity:0;position:absolute;transition:opacity .2s linear .1s;visibility:hidden}.component-indicator::before{border:1px solid #adadad;border-radius:1px;box-shadow:0 0 2px rgba(0,0,0,.4);content:attr(data-label);font-size:1.2rem;margin:30px 0 0 -10px;min-width:50px;padding:4px 5px}.component-indicator::after{border-color:#999;border-style:solid;border-width:1px 0 0 1px;box-shadow:-1px -1px 1px rgba(0,0,0,.1);content:'';height:10px;margin:9px 0 0 5px;-ms-transform:rotate(45deg);transform:rotate(45deg);width:10px}.component-indicator:hover::after,.component-indicator:hover::before{opacity:1;transition:opacity .2s linear;visibility:visible}.component-indicator span{display:block;height:16px;overflow:hidden;width:16px}.component-indicator span:before{content:'';display:block;font-family:Icons;font-size:16px;height:100%;line-height:16px;width:100%}.component-indicator._on{background:#79a22e}.component-indicator._off{background:#e22626}.component-indicator._off span:before{background:#fff;height:4px;margin:6px auto 20px;width:12px}.component-indicator._info{background:0 0}.component-indicator._info span{width:21px}.component-indicator._info span:before{color:#008bdb;content:'\e648';font-family:Icons;font-size:16px}.component-indicator._tooltip{background:0 0;margin:0 0 8px 5px}.component-indicator._tooltip a{width:21px}.component-indicator._tooltip a:hover{text-decoration:none}.component-indicator._tooltip a:before{color:#514943;content:'\e633';font-family:Icons;font-size:16px}.col-manager-item-name .data-grid-data{padding-left:5px}.col-manager-item-name .ng-hide+.data-grid-data{padding-left:24px}.col-manager-item-name ._hide-dependencies,.col-manager-item-name ._show-dependencies{cursor:pointer;padding-left:24px;position:relative}.col-manager-item-name ._hide-dependencies:before,.col-manager-item-name ._show-dependencies:before{display:block;font-family:Icons;font-size:12px;left:0;position:absolute;top:1px}.col-manager-item-name ._show-dependencies:before{content:'\e62b'}.col-manager-item-name ._hide-dependencies:before{content:'\e628'}.col-manager-item-name ._no-dependencies{padding-left:24px}.product-modules-block{font-size:1.2rem;padding:15px 0 0}.col-manager-item-name .product-modules-block{padding-left:1rem}.product-modules-descriprion,.product-modules-title{font-weight:700;margin:0 0 7px}.product-modules-list{font-size:1.1rem;list-style:none;margin:0}.col-manager-item-name .product-modules-list{margin-left:15px}.col-manager-item-name .product-modules-list li{padding:0 0 0 15px;position:relative}.product-modules-list li{margin:0 0 .5rem}.product-modules-list .component-indicator{height:10px;left:0;position:absolute;top:3px;width:10px}.module-summary{white-space:nowrap}.module-summary-title{font-size:2.1rem;margin-right:1rem}.app-updater .nav{display:block;margin-bottom:3.1rem;margin-top:-2.8rem}.app-updater .nav-bar-outer-actions{margin-top:1rem;padding-right:0}.app-updater .nav-bar-outer-actions .btn-wrap-cancel{margin-right:2.6rem}.main{padding-bottom:2rem;padding-top:3rem}.menu-wrapper .logo-static{pointer-events:none}.header{display:none}.header .logo{float:left;height:4.1rem;width:3.5rem}.header-title{font-size:2.8rem;letter-spacing:.02em;line-height:1.4;margin:2.5rem 0 3.5rem 5rem}.page-title{margin-bottom:1rem}.page-sub-title{font-size:2rem}.accent-box{margin-bottom:2rem}.accent-box .btn-prime{margin-top:1.5rem}.spinner.side{float:left;font-size:2.4rem;margin-left:2rem;margin-top:-5px}.page-landing{margin:7.6% auto 0;max-width:44rem;text-align:center}.page-landing .logo{height:5.6rem;margin-bottom:2rem;width:19.2rem}.page-landing .text-version{margin-bottom:3rem}.page-landing .text-welcome{margin-bottom:6.5rem}.page-landing .text-terms{margin-bottom:2.5rem;text-align:center}.page-landing .btn-submit,.page-license .license-text{margin-bottom:2rem}.page-license .page-license-footer{text-align:right}.readiness-check-item{margin-bottom:4rem;min-height:2.5rem}.readiness-check-item .spinner{float:left;font-size:2.5rem;margin:-.4rem 0 0 1.7rem}.readiness-check-title{font-size:1.4rem;font-weight:700;margin-bottom:.1rem;margin-left:5.7rem}.readiness-check-content{margin-left:5.7rem;margin-right:22rem;position:relative}.readiness-check-content .readiness-check-title{margin-left:0}.readiness-check-content .list{margin-top:-.3rem}.readiness-check-side{left:100%;padding-left:2.4rem;position:absolute;top:0;width:22rem}.readiness-check-side .side-title{margin-bottom:0}.readiness-check-icon{float:left;margin-left:1.7rem;margin-top:.3rem}.extensions-information{margin-bottom:5rem}.extensions-information h3{font-size:1.4rem;margin-bottom:1.3rem}.extensions-information .message{margin-bottom:2.5rem}.extensions-information .message:before{margin-top:0;top:1.8rem}.extensions-information .extensions-container{padding:0 2rem}.extensions-information .list{margin-bottom:1rem}.extensions-information .list select{cursor:pointer}.extensions-information .list select:disabled{background:#ccc;cursor:default}.extensions-information .list .extension-delete{font-size:1.7rem;padding-top:0}.delete-modal-wrap{padding:0 4% 4rem}.delete-modal-wrap h3{font-size:3.4rem;display:inline-block;font-weight:300;margin:0 0 2rem;padding:.9rem 0 0;vertical-align:top}.delete-modal-wrap .actions{padding:3rem 0 0}.page-web-configuration .form-el-insider-wrap{width:auto}.page-web-configuration .form-el-insider{width:15.4rem}.page-web-configuration .form-el-insider-input .form-el-input{width:16.5rem}.customize-your-store .advanced-modules-count,.customize-your-store .advanced-modules-select{padding-left:1.5rem}.customize-your-store .customize-your-store-advanced{min-width:0}.customize-your-store .message-error:before{margin-top:0;top:1.8rem}.customize-your-store .message-error a{color:#333;text-decoration:underline}.customize-your-store .message-error .form-label:before{background:#fff}.customize-your-store .customize-database-clean p{margin-top:2.5rem}.content-install{margin-bottom:2rem}.console{border:1px solid #ccc;font-family:'Courier New',Courier,monospace;font-weight:300;height:20rem;margin:1rem 0 2rem;overflow-y:auto;padding:1.5rem 2rem 2rem;resize:vertical}.console .text-danger{color:#e22626}.console .text-success{color:#090}.console .hidden{display:none}.content-success .btn-prime{margin-top:1.5rem}.jumbo-title{font-size:3.6rem}.jumbo-title .jumbo-icon{font-size:3.8rem;margin-right:.25em;position:relative;top:.15em}.install-database-clean{margin-top:4rem}.install-database-clean .btn{margin-right:1rem}.page-sub-title{margin-bottom:2.1rem;margin-top:3rem}.multiselect-custom{max-width:71.1rem}.content-install{margin-top:3.7rem}.home-page-inner-wrap{margin:0 auto;max-width:91rem}.setup-home-title{margin-bottom:3.9rem;padding-top:1.8rem;text-align:center}.setup-home-item{background-color:#fafafa;border:1px solid #ccc;color:#333;display:block;margin-bottom:2rem;margin-left:1.3rem;margin-right:1.3rem;min-height:30rem;padding:2rem;text-align:center}.setup-home-item:hover{border-color:#8c8c8c;color:#333;text-decoration:none;transition:border-color .1s linear}.setup-home-item:active{-ms-transform:scale(0.99);transform:scale(0.99)}.setup-home-item:before{display:block;font-size:7rem;margin-bottom:3.3rem;margin-top:4rem}.setup-home-item-component:before,.setup-home-item-extension:before{content:'\e612'}.setup-home-item-module:before{content:'\e647'}.setup-home-item-upgrade:before{content:'\e614'}.setup-home-item-configuration:before{content:'\e610'}.setup-home-item-title{display:block;font-size:1.8rem;letter-spacing:.025em;margin-bottom:1rem}.setup-home-item-description{display:block}.extension-manager-wrap{border:1px solid #bbb;margin:0 0 4rem}.extension-manager-account{font-size:2.1rem;display:inline-block;font-weight:400}.extension-manager-title{font-size:3.2rem;background-color:#f8f8f8;border-bottom:1px solid #e3e3e3;color:#41362f;font-weight:600;line-height:1.2;padding:2rem}.extension-manager-content{padding:2.5rem 2rem 2rem}.extension-manager-items{list-style:none;margin:0;text-align:center}.extension-manager-items .btn{border:1px solid #adadad;display:block;margin:1rem auto 0}.extension-manager-items .item-title{font-size:2.1rem;display:inline-block;text-align:left}.extension-manager-items .item-number{font-size:4.1rem;display:inline-block;line-height:.8;margin:0 5px 1.5rem 0;vertical-align:top}.extension-manager-items .item-date{font-size:2.6rem;margin-top:1px}.extension-manager-items .item-date-title{font-size:1.5rem}.extension-manager-items .item-install{margin:0 0 2rem}.sync-login-wrap{padding:0 10% 4rem}.sync-login-wrap .legend{font-size:2.6rem;color:#eb5202;float:left;font-weight:300;line-height:1.2;margin:-1rem 0 2.5rem;position:static;width:100%}.sync-login-wrap .legend._hidden{display:none}.sync-login-wrap .login-header{font-size:3.4rem;font-weight:300;margin:0 0 2rem}.sync-login-wrap .login-header span{display:inline-block;padding:.9rem 0 0;vertical-align:top}.sync-login-wrap h4{font-size:1.4rem;margin:0 0 2rem}.sync-login-wrap .sync-login-steps{margin:0 0 2rem 1.5rem}.sync-login-wrap .sync-login-steps li{padding:0 0 0 1rem}.sync-login-wrap .form-row .form-label{display:inline-block}.sync-login-wrap .form-row .form-label.required{padding-left:1.5rem}.sync-login-wrap .form-row .form-label.required:after{left:0;position:absolute;right:auto}.sync-login-wrap .form-row{max-width:28rem}.sync-login-wrap .form-actions{display:table;margin-top:-1.3rem}.sync-login-wrap .form-actions .links{display:table-header-group}.sync-login-wrap .form-actions .actions{padding:3rem 0 0}@media all and (max-width:1047px){.admin__menu .submenu li{min-width:19.8rem}.nav{padding-bottom:5.38rem;padding-left:1.5rem;text-align:center}.nav-bar{display:inline-block;float:none;margin-right:0;vertical-align:top}.nav .btn-group,.nav-bar-outer-actions{display:inline-block;float:none;margin-top:-8.48rem;text-align:center;vertical-align:top;width:100%}.nav-bar-outer-actions{padding-right:0}.nav-bar-outer-actions .outer-actions-inner-wrap{display:inline-block}.app-updater .nav{padding-bottom:1.7rem}.app-updater .nav-bar-outer-actions{margin-top:2rem}}@media all and (min-width:768px){.page-layout-admin-2columns-left .page-columns{margin-left:-30px}.page-layout-admin-2columns-left .page-columns:after{clear:both;content:'';display:table}.page-layout-admin-2columns-left .page-columns .main-col{width:calc((100%) * .75 - 30px);float:right}.page-layout-admin-2columns-left .page-columns .side-col{width:calc((100%) * .25 - 30px);float:left;margin-left:30px}.col-m-1,.col-m-10,.col-m-11,.col-m-12,.col-m-2,.col-m-3,.col-m-4,.col-m-5,.col-m-6,.col-m-7,.col-m-8,.col-m-9{float:left}.col-m-12{width:100%}.col-m-11{width:91.66666667%}.col-m-10{width:83.33333333%}.col-m-9{width:75%}.col-m-8{width:66.66666667%}.col-m-7{width:58.33333333%}.col-m-6{width:50%}.col-m-5{width:41.66666667%}.col-m-4{width:33.33333333%}.col-m-3{width:25%}.col-m-2{width:16.66666667%}.col-m-1{width:8.33333333%}.col-m-pull-12{right:100%}.col-m-pull-11{right:91.66666667%}.col-m-pull-10{right:83.33333333%}.col-m-pull-9{right:75%}.col-m-pull-8{right:66.66666667%}.col-m-pull-7{right:58.33333333%}.col-m-pull-6{right:50%}.col-m-pull-5{right:41.66666667%}.col-m-pull-4{right:33.33333333%}.col-m-pull-3{right:25%}.col-m-pull-2{right:16.66666667%}.col-m-pull-1{right:8.33333333%}.col-m-pull-0{right:auto}.col-m-push-12{left:100%}.col-m-push-11{left:91.66666667%}.col-m-push-10{left:83.33333333%}.col-m-push-9{left:75%}.col-m-push-8{left:66.66666667%}.col-m-push-7{left:58.33333333%}.col-m-push-6{left:50%}.col-m-push-5{left:41.66666667%}.col-m-push-4{left:33.33333333%}.col-m-push-3{left:25%}.col-m-push-2{left:16.66666667%}.col-m-push-1{left:8.33333333%}.col-m-push-0{left:auto}.col-m-offset-12{margin-left:100%}.col-m-offset-11{margin-left:91.66666667%}.col-m-offset-10{margin-left:83.33333333%}.col-m-offset-9{margin-left:75%}.col-m-offset-8{margin-left:66.66666667%}.col-m-offset-7{margin-left:58.33333333%}.col-m-offset-6{margin-left:50%}.col-m-offset-5{margin-left:41.66666667%}.col-m-offset-4{margin-left:33.33333333%}.col-m-offset-3{margin-left:25%}.col-m-offset-2{margin-left:16.66666667%}.col-m-offset-1{margin-left:8.33333333%}.col-m-offset-0{margin-left:0}.page-columns{margin-left:-30px}.page-columns:after{clear:both;content:'';display:table}.page-columns .page-inner-content{width:calc((100%) * .75 - 30px);float:right}.page-columns .page-inner-sidebar{width:calc((100%) * .25 - 30px);float:left;margin-left:30px}}@media all and (min-width:1048px){.col-l-1,.col-l-10,.col-l-11,.col-l-12,.col-l-2,.col-l-3,.col-l-4,.col-l-5,.col-l-6,.col-l-7,.col-l-8,.col-l-9{float:left}.col-l-12{width:100%}.col-l-11{width:91.66666667%}.col-l-10{width:83.33333333%}.col-l-9{width:75%}.col-l-8{width:66.66666667%}.col-l-7{width:58.33333333%}.col-l-6{width:50%}.col-l-5{width:41.66666667%}.col-l-4{width:33.33333333%}.col-l-3{width:25%}.col-l-2{width:16.66666667%}.col-l-1{width:8.33333333%}.col-l-pull-12{right:100%}.col-l-pull-11{right:91.66666667%}.col-l-pull-10{right:83.33333333%}.col-l-pull-9{right:75%}.col-l-pull-8{right:66.66666667%}.col-l-pull-7{right:58.33333333%}.col-l-pull-6{right:50%}.col-l-pull-5{right:41.66666667%}.col-l-pull-4{right:33.33333333%}.col-l-pull-3{right:25%}.col-l-pull-2{right:16.66666667%}.col-l-pull-1{right:8.33333333%}.col-l-pull-0{right:auto}.col-l-push-12{left:100%}.col-l-push-11{left:91.66666667%}.col-l-push-10{left:83.33333333%}.col-l-push-9{left:75%}.col-l-push-8{left:66.66666667%}.col-l-push-7{left:58.33333333%}.col-l-push-6{left:50%}.col-l-push-5{left:41.66666667%}.col-l-push-4{left:33.33333333%}.col-l-push-3{left:25%}.col-l-push-2{left:16.66666667%}.col-l-push-1{left:8.33333333%}.col-l-push-0{left:auto}.col-l-offset-12{margin-left:100%}.col-l-offset-11{margin-left:91.66666667%}.col-l-offset-10{margin-left:83.33333333%}.col-l-offset-9{margin-left:75%}.col-l-offset-8{margin-left:66.66666667%}.col-l-offset-7{margin-left:58.33333333%}.col-l-offset-6{margin-left:50%}.col-l-offset-5{margin-left:41.66666667%}.col-l-offset-4{margin-left:33.33333333%}.col-l-offset-3{margin-left:25%}.col-l-offset-2{margin-left:16.66666667%}.col-l-offset-1{margin-left:8.33333333%}.col-l-offset-0{margin-left:0}}@media all and (min-width:1440px){.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{float:left}.col-xl-12{width:100%}.col-xl-11{width:91.66666667%}.col-xl-10{width:83.33333333%}.col-xl-9{width:75%}.col-xl-8{width:66.66666667%}.col-xl-7{width:58.33333333%}.col-xl-6{width:50%}.col-xl-5{width:41.66666667%}.col-xl-4{width:33.33333333%}.col-xl-3{width:25%}.col-xl-2{width:16.66666667%}.col-xl-1{width:8.33333333%}.col-xl-pull-12{right:100%}.col-xl-pull-11{right:91.66666667%}.col-xl-pull-10{right:83.33333333%}.col-xl-pull-9{right:75%}.col-xl-pull-8{right:66.66666667%}.col-xl-pull-7{right:58.33333333%}.col-xl-pull-6{right:50%}.col-xl-pull-5{right:41.66666667%}.col-xl-pull-4{right:33.33333333%}.col-xl-pull-3{right:25%}.col-xl-pull-2{right:16.66666667%}.col-xl-pull-1{right:8.33333333%}.col-xl-pull-0{right:auto}.col-xl-push-12{left:100%}.col-xl-push-11{left:91.66666667%}.col-xl-push-10{left:83.33333333%}.col-xl-push-9{left:75%}.col-xl-push-8{left:66.66666667%}.col-xl-push-7{left:58.33333333%}.col-xl-push-6{left:50%}.col-xl-push-5{left:41.66666667%}.col-xl-push-4{left:33.33333333%}.col-xl-push-3{left:25%}.col-xl-push-2{left:16.66666667%}.col-xl-push-1{left:8.33333333%}.col-xl-push-0{left:auto}.col-xl-offset-12{margin-left:100%}.col-xl-offset-11{margin-left:91.66666667%}.col-xl-offset-10{margin-left:83.33333333%}.col-xl-offset-9{margin-left:75%}.col-xl-offset-8{margin-left:66.66666667%}.col-xl-offset-7{margin-left:58.33333333%}.col-xl-offset-6{margin-left:50%}.col-xl-offset-5{margin-left:41.66666667%}.col-xl-offset-4{margin-left:33.33333333%}.col-xl-offset-3{margin-left:25%}.col-xl-offset-2{margin-left:16.66666667%}.col-xl-offset-1{margin-left:8.33333333%}.col-xl-offset-0{margin-left:0}}@media all and (max-width:767px){.abs-clearer-mobile:after,.nav-bar:after{clear:both;content:'';display:table}.list-definition>dt{float:none}.list-definition>dd{margin-left:0}.form-row .form-label{text-align:left}.form-row .form-label.required:after{position:static}.nav{padding-bottom:0;padding-left:0;padding-right:0}.nav-bar-outer-actions{margin-top:0}.nav-bar{display:block;margin-bottom:0;margin-left:auto;margin-right:auto;width:30.9rem}.nav-bar:before{display:none}.nav-bar>li{float:left;min-height:9rem}.nav-bar>li:after{display:none}.nav-bar>li:nth-child(4n){clear:both}.nav-bar a{line-height:1.4}.tooltip{display:none!important}.readiness-check-content{margin-right:2rem}.readiness-check-side{padding:2rem 0;position:static}.form-el-insider,.form-el-insider-wrap,.page-web-configuration .form-el-insider-input,.page-web-configuration .form-el-insider-input .form-el-input{display:block;width:100%}}@media all and (max-width:479px){.nav-bar{width:23.175rem}.nav-bar>li{width:7.725rem}.nav .btn-group .btn-wrap-try-again,.nav-bar-outer-actions .btn-wrap-try-again{clear:both;display:block;float:none;margin-left:auto;margin-right:auto;margin-top:1rem;padding-top:1rem}} diff --git a/setup/src/Magento/Setup/Console/Command/InstallCommand.php b/setup/src/Magento/Setup/Console/Command/InstallCommand.php index 74c2e3b24234c..cc1cca74ed6df 100644 --- a/setup/src/Magento/Setup/Console/Command/InstallCommand.php +++ b/setup/src/Magento/Setup/Console/Command/InstallCommand.php @@ -183,7 +183,7 @@ protected function configure() self::INPUT_KEY_INTERACTIVE_SETUP, self::INPUT_KEY_INTERACTIVE_SETUP_SHORTCUT, InputOption::VALUE_NONE, - 'Interactive Magento instalation' + 'Interactive Magento installation' ), new InputOption( OperationsExecutor::KEY_SAFE_MODE, diff --git a/setup/src/Magento/Setup/Controller/Session.php b/setup/src/Magento/Setup/Controller/Session.php index e310dd485ace5..c9caa5a8de792 100644 --- a/setup/src/Magento/Setup/Controller/Session.php +++ b/setup/src/Magento/Setup/Controller/Session.php @@ -5,6 +5,9 @@ */ namespace Magento\Setup\Controller; +/** + * Sets up session for setup/index.php/session/prolong or redirects to error page + */ class Session extends \Zend\Mvc\Controller\AbstractActionController { /** @@ -52,23 +55,28 @@ public function prolongAction() try { if ($this->serviceManager->get(\Magento\Framework\App\DeploymentConfig::class)->isAvailable()) { $objectManager = $this->objectManagerProvider->get(); - /** @var \Magento\Framework\App\State $adminAppState */ - $adminAppState = $objectManager->get(\Magento\Framework\App\State::class); - $adminAppState->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML); - $sessionConfig = $objectManager->get(\Magento\Backend\Model\Session\AdminConfig::class); - /** @var \Magento\Backend\Model\Url $backendUrl */ - $backendUrl = $objectManager->get(\Magento\Backend\Model\Url::class); - $urlPath = parse_url($backendUrl->getBaseUrl(), PHP_URL_PATH); - $cookiePath = $urlPath . 'setup'; - $sessionConfig->setCookiePath($cookiePath); /* @var \Magento\Backend\Model\Auth\Session $session */ - $session = $objectManager->create( - \Magento\Backend\Model\Auth\Session::class, - [ - 'sessionConfig' => $sessionConfig, - 'appState' => $adminAppState - ] - ); + $session = $objectManager->get(\Magento\Backend\Model\Auth\Session::class); + // check if session was already set in \Magento\Setup\Mvc\Bootstrap\InitParamListener::authPreDispatch + if (!$session->isSessionExists()) { + /** @var \Magento\Framework\App\State $adminAppState */ + $adminAppState = $objectManager->get(\Magento\Framework\App\State::class); + $adminAppState->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML); + $sessionConfig = $objectManager->get(\Magento\Backend\Model\Session\AdminConfig::class); + /** @var \Magento\Backend\Model\Url $backendUrl */ + $backendUrl = $objectManager->get(\Magento\Backend\Model\Url::class); + $urlPath = parse_url($backendUrl->getBaseUrl(), PHP_URL_PATH); + $cookiePath = $urlPath . 'setup'; + $sessionConfig->setCookiePath($cookiePath); + /* @var \Magento\Backend\Model\Auth\Session $session */ + $session = $objectManager->create( + \Magento\Backend\Model\Auth\Session::class, + [ + 'sessionConfig' => $sessionConfig, + 'appState' => $adminAppState + ] + ); + } $session->prolong(); return new \Zend\View\Model\JsonModel(['success' => true]); } @@ -78,6 +86,8 @@ public function prolongAction() } /** + * Unlogin action, return 401 error page + * * @return \Zend\View\Model\ViewModel|\Zend\Http\Response */ public function unloginAction() diff --git a/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php b/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php new file mode 100644 index 0000000000000..e60a2c9f30765 --- /dev/null +++ b/setup/src/Magento/Setup/Fixtures/CouponCodesFixture.php @@ -0,0 +1,168 @@ + + * {int} + * + * @see setup/performance-toolkit/profiles/ce/small.xml + */ +class CouponCodesFixture extends Fixture +{ + /** + * @var int + */ + protected $priority = 129; + + /** + * @var int + */ + protected $couponCodesCount = 0; + + /** + * @var \Magento\SalesRule\Model\RuleFactory + */ + private $ruleFactory; + + /** + * @var \Magento\SalesRule\Model\CouponFactory + */ + private $couponCodeFactory; + + /** + * Constructor + * + * @param FixtureModel $fixtureModel + * @param \Magento\SalesRule\Model\RuleFactory|null $ruleFactory + * @param \Magento\SalesRule\Model\CouponFactory|null $couponCodeFactory + */ + public function __construct( + FixtureModel $fixtureModel, + \Magento\SalesRule\Model\RuleFactory $ruleFactory = null, + \Magento\SalesRule\Model\CouponFactory $couponCodeFactory = null + ) { + parent::__construct($fixtureModel); + $this->ruleFactory = $ruleFactory ?: $this->fixtureModel->getObjectManager() + ->get(\Magento\SalesRule\Model\RuleFactory::class); + $this->couponCodeFactory = $couponCodeFactory ?: $this->fixtureModel->getObjectManager() + ->get(\Magento\SalesRule\Model\CouponFactory::class); + } + + /** + * @inheritdoc + * + * @SuppressWarnings(PHPMD) + */ + public function execute() + { + $this->fixtureModel->resetObjectManager(); + $this->couponCodesCount = $this->fixtureModel->getValue('coupon_codes', 0); + if (!$this->couponCodesCount) { + return; + } + + /** @var \Magento\Store\Model\StoreManager $storeManager */ + $storeManager = $this->fixtureModel->getObjectManager()->create(\Magento\Store\Model\StoreManager::class); + /** @var $category \Magento\Catalog\Model\Category */ + $category = $this->fixtureModel->getObjectManager()->get(\Magento\Catalog\Model\Category::class); + + //Get all websites + $categoriesArray = []; + $websites = $storeManager->getWebsites(); + foreach ($websites as $website) { + //Get all groups + $websiteGroups = $website->getGroups(); + foreach ($websiteGroups as $websiteGroup) { + $websiteGroupRootCategory = $websiteGroup->getRootCategoryId(); + $category->load($websiteGroupRootCategory); + $categoryResource = $category->getResource(); + //Get all categories + $resultsCategories = $categoryResource->getAllChildren($category); + foreach ($resultsCategories as $resultsCategory) { + $category->load($resultsCategory); + $structure = explode('/', $category->getPath()); + if (count($structure) > 2) { + $categoriesArray[] = [$category->getId(), $website->getId()]; + } + } + } + } + asort($categoriesArray); + $categoriesArray = array_values($categoriesArray); + + $this->generateCouponCodes($this->ruleFactory, $this->couponCodeFactory, $categoriesArray); + } + + /** + * Generate Coupon Codes + * + * @param \Magento\SalesRule\Model\RuleFactory $ruleFactory + * @param \Magento\SalesRule\Model\CouponFactory $couponCodeFactory + * @param array $categoriesArray + * + * @return void + */ + public function generateCouponCodes($ruleFactory, $couponCodeFactory, $categoriesArray) + { + for ($i = 0; $i < $this->couponCodesCount; $i++) { + $ruleName = sprintf('Coupon Code %1$d', $i); + $data = [ + 'rule_id' => null, + 'name' => $ruleName, + 'is_active' => '1', + 'website_ids' => $categoriesArray[$i % count($categoriesArray)][1], + 'customer_group_ids' => [ + 0 => '0', + 1 => '1', + 2 => '2', + 3 => '3', + ], + 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC, + 'conditions' => [], + 'simple_action' => \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION, + 'discount_amount' => 5, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + ]; + + $model = $ruleFactory->create(); + $model->loadPost($data); + $useAutoGeneration = (int)!empty($data['use_auto_generation']); + $model->setUseAutoGeneration($useAutoGeneration); + $model->save(); + + $coupon = $couponCodeFactory->create(); + $coupon->setRuleId($model->getId()) + ->setCode('CouponCode' . $i) + ->setIsPrimary(true) + ->setType(0); + $coupon->save(); + } + } + + /** + * @inheritdoc + */ + public function getActionTitle() + { + return 'Generating coupon codes'; + } + + /** + * @inheritdoc + */ + public function introduceParamLabels() + { + return [ + 'coupon_codes' => 'Coupon Codes' + ]; + } +} diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php index fa79139e73313..3f2aedae1373c 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php @@ -8,6 +8,7 @@ use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Encryption\KeyValidator; use Magento\Framework\Setup\ConfigOptionsListInterface; use Magento\Framework\Setup\Option\FlagConfigOption; use Magento\Framework\Setup\Option\SelectConfigOption; @@ -38,13 +39,19 @@ class ConfigOptionsList implements ConfigOptionsListInterface */ private $configOptionsCollection = []; + /** + * @var KeyValidator + */ + private $encryptionKeyValidator; + /** * @var array */ private $configOptionsListClasses = [ \Magento\Setup\Model\ConfigOptionsList\Session::class, \Magento\Setup\Model\ConfigOptionsList\Cache::class, - \Magento\Setup\Model\ConfigOptionsList\PageCache::class + \Magento\Setup\Model\ConfigOptionsList\PageCache::class, + \Magento\Setup\Model\ConfigOptionsList\Lock::class, ]; /** @@ -52,18 +59,25 @@ class ConfigOptionsList implements ConfigOptionsListInterface * * @param ConfigGenerator $configGenerator * @param DbValidator $dbValidator + * @param KeyValidator|null $encryptionKeyValidator */ - public function __construct(ConfigGenerator $configGenerator, DbValidator $dbValidator) - { + public function __construct( + ConfigGenerator $configGenerator, + DbValidator $dbValidator, + KeyValidator $encryptionKeyValidator = null + ) { $this->configGenerator = $configGenerator; $this->dbValidator = $dbValidator; + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); foreach ($this->configOptionsListClasses as $className) { - $this->configOptionsCollection[] = \Magento\Framework\App\ObjectManager::getInstance()->get($className); + $this->configOptionsCollection[] = $objectManager->get($className); } + $this->encryptionKeyValidator = $encryptionKeyValidator ?: $objectManager->get(KeyValidator::class); } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getOptions() @@ -160,7 +174,7 @@ public function getOptions() } /** - * {@inheritdoc} + * @inheritdoc */ public function createConfig(array $data, DeploymentConfig $deploymentConfig) { @@ -184,7 +198,7 @@ public function createConfig(array $data, DeploymentConfig $deploymentConfig) } /** - * {@inheritdoc} + * @inheritdoc */ public function validate(array $options, DeploymentConfig $deploymentConfig) { @@ -276,8 +290,9 @@ private function validateEncryptionKey(array $options) $errors = []; if (isset($options[ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY]) - && !$options[ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY]) { - $errors[] = 'Invalid encryption key'; + && !$this->encryptionKeyValidator->isValid($options[ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY]) + ) { + $errors[] = 'Invalid encryption key. Encryption key must be 32 character string without any white space.'; } return $errors; diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php index 173064b472217..89a37429c47c9 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php @@ -27,6 +27,8 @@ class Cache implements ConfigOptionsListInterface const INPUT_KEY_CACHE_BACKEND_REDIS_DATABASE = 'cache-backend-redis-db'; const INPUT_KEY_CACHE_BACKEND_REDIS_PORT = 'cache-backend-redis-port'; const INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD = 'cache-backend-redis-password'; + const INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESS_DATA = 'cache-backend-redis-compress-data'; + const INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESSION_LIB = 'cache-backend-redis-compression-lib'; const INPUT_KEY_CACHE_ID_PREFIX = 'cache-id-prefix'; const CONFIG_PATH_CACHE_BACKEND = 'cache/frontend/default/backend'; @@ -34,6 +36,8 @@ class Cache implements ConfigOptionsListInterface const CONFIG_PATH_CACHE_BACKEND_DATABASE = 'cache/frontend/default/backend_options/database'; const CONFIG_PATH_CACHE_BACKEND_PORT = 'cache/frontend/default/backend_options/port'; const CONFIG_PATH_CACHE_BACKEND_PASSWORD = 'cache/frontend/default/backend_options/password'; + const CONFIG_PATH_CACHE_BACKEND_COMPRESS_DATA = 'cache/frontend/default/backend_options/compress_data'; + const CONFIG_PATH_CACHE_BACKEND_COMPRESSION_LIB = 'cache/frontend/default/backend_options/compression_lib'; const CONFIG_PATH_CACHE_ID_PREFIX = 'cache/frontend/default/id_prefix'; /** @@ -43,7 +47,9 @@ class Cache implements ConfigOptionsListInterface self::INPUT_KEY_CACHE_BACKEND_REDIS_SERVER => '127.0.0.1', self::INPUT_KEY_CACHE_BACKEND_REDIS_DATABASE => '0', self::INPUT_KEY_CACHE_BACKEND_REDIS_PORT => '6379', - self::INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD => '' + self::INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD => '', + self::INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESS_DATA => '1', + self::INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESSION_LIB => '', ]; /** @@ -60,7 +66,9 @@ class Cache implements ConfigOptionsListInterface self::INPUT_KEY_CACHE_BACKEND_REDIS_SERVER => self::CONFIG_PATH_CACHE_BACKEND_SERVER, self::INPUT_KEY_CACHE_BACKEND_REDIS_DATABASE => self::CONFIG_PATH_CACHE_BACKEND_DATABASE, self::INPUT_KEY_CACHE_BACKEND_REDIS_PORT => self::CONFIG_PATH_CACHE_BACKEND_PORT, - self::INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD => self::CONFIG_PATH_CACHE_BACKEND_PASSWORD + self::INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD => self::CONFIG_PATH_CACHE_BACKEND_PASSWORD, + self::INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESS_DATA => self::CONFIG_PATH_CACHE_BACKEND_COMPRESS_DATA, + self::INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESSION_LIB => self::CONFIG_PATH_CACHE_BACKEND_COMPRESSION_LIB, ]; /** @@ -115,12 +123,24 @@ public function getOptions() self::CONFIG_PATH_CACHE_BACKEND_PASSWORD, 'Redis server password' ), + new TextConfigOption( + self::INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESS_DATA, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_CACHE_BACKEND_COMPRESS_DATA, + 'Set to 0 to disable compression (default is 1, enabled)' + ), + new TextConfigOption( + self::INPUT_KEY_CACHE_BACKEND_REDIS_COMPRESSION_LIB, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_CACHE_BACKEND_COMPRESSION_LIB, + 'Compression lib to use [snappy,lzf,l4z,zstd,gzip] (leave blank to determine automatically)' + ), new TextConfigOption( self::INPUT_KEY_CACHE_ID_PREFIX, TextConfigOption::FRONTEND_WIZARD_TEXT, self::CONFIG_PATH_CACHE_ID_PREFIX, 'ID prefix for cache keys' - ) + ), ]; } diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php new file mode 100644 index 0000000000000..66f41128c46b1 --- /dev/null +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php @@ -0,0 +1,342 @@ + [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_DB_PREFIX => self::CONFIG_PATH_LOCK_DB_PREFIX, + ], + LockBackendFactory::LOCK_ZOOKEEPER => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_ZOOKEEPER_HOST => self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + ], + LockBackendFactory::LOCK_CACHE => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + ], + LockBackendFactory::LOCK_FILE => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_FILE_PATH => self::CONFIG_PATH_LOCK_FILE_PATH, + ], + ]; + + /** + * The list of default values + * + * @var array + */ + private $defaultConfigValues = [ + self::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, + self::INPUT_KEY_LOCK_DB_PREFIX => null, + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => ZookeeperLock::DEFAULT_PATH, + ]; + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_LOCK_PROVIDER, + SelectConfigOption::FRONTEND_WIZARD_SELECT, + $this->validLockProviders, + self::CONFIG_PATH_LOCK_PROVIDER, + 'Lock provider name', + LockBackendFactory::LOCK_DB + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_DB_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_DB_PREFIX, + 'Installation specific lock prefix to avoid lock conflicts' + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_ZOOKEEPER_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + 'Host and port to connect to Zookeeper cluster. For example: 127.0.0.1:2181' + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + 'The path where Zookeeper will save locks. The default path is: ' . ZookeeperLock::DEFAULT_PATH + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_FILE_PATH, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_FILE_PATH, + 'The path where file locks will be saved.' + ), + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $lockProvider = $this->getLockProvider($options, $deploymentConfig); + + $this->setDefaultConfiguration($configData, $deploymentConfig, $lockProvider); + + foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { + if (isset($options[$input])) { + $configData->set($path, $options[$input]); + } + } + + return $configData; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + $lockProvider = $this->getLockProvider($options, $deploymentConfig); + switch ($lockProvider) { + case LockBackendFactory::LOCK_ZOOKEEPER: + $errors = $this->validateZookeeperConfig($options, $deploymentConfig); + break; + case LockBackendFactory::LOCK_FILE: + $errors = $this->validateFileConfig($options, $deploymentConfig); + break; + case LockBackendFactory::LOCK_CACHE: + case LockBackendFactory::LOCK_DB: + $errors = []; + break; + default: + $errors[] = 'The lock provider ' . $lockProvider . ' does not exist.'; + } + + return $errors; + } + + /** + * Validates File locks configuration + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array + */ + private function validateFileConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $errors = []; + + $path = $options[self::INPUT_KEY_LOCK_FILE_PATH] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_FILE_PATH, + $this->getDefaultValue(self::INPUT_KEY_LOCK_FILE_PATH) + ); + + if (!$path) { + $errors[] = 'The path needs to be a non-empty string.'; + } + + return $errors; + } + + /** + * Validates Zookeeper configuration + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array + */ + private function validateZookeeperConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $errors = []; + + if (!extension_loaded(LockBackendFactory::LOCK_ZOOKEEPER)) { + $errors[] = 'php extension Zookeeper is not installed.'; + } + + $host = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_HOST] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_HOST) + ); + $path = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_PATH] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_PATH) + ); + + if (!$path) { + $errors[] = 'Zookeeper path needs to be a non-empty string.'; + } + + if (!$host) { + $errors[] = 'Zookeeper host is should be set.'; + } + + return $errors; + } + + /** + * Returns the name of lock provider + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return string + */ + private function getLockProvider(array $options, DeploymentConfig $deploymentConfig): string + { + if (!isset($options[self::INPUT_KEY_LOCK_PROVIDER])) { + return (string) $deploymentConfig->get( + self::CONFIG_PATH_LOCK_PROVIDER, + $this->getDefaultValue(self::INPUT_KEY_LOCK_PROVIDER) + ); + } + + return (string) $options[self::INPUT_KEY_LOCK_PROVIDER]; + } + + /** + * Sets default configuration for locks + * + * @param ConfigData $configData + * @param DeploymentConfig $deploymentConfig + * @param string $lockProvider + * @return ConfigData + */ + private function setDefaultConfiguration( + ConfigData $configData, + DeploymentConfig $deploymentConfig, + string $lockProvider + ) { + foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { + $configData->set($path, $deploymentConfig->get($path, $this->getDefaultValue($input))); + } + + return $configData; + } + + /** + * Returns default value by input key + * + * If default value is not set returns null + * + * @param string $inputKey + * @return mixed|null + */ + private function getDefaultValue(string $inputKey) + { + if (isset($this->defaultConfigValues[$inputKey])) { + return $this->defaultConfigValues[$inputKey]; + } else { + return null; + } + } +} diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php index 7451b59356828..65bfc650c0206 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php @@ -6,10 +6,10 @@ namespace Magento\Setup\Model\ConfigOptionsList; -use Magento\Framework\Setup\ConfigOptionsListInterface; -use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; use Magento\Framework\Setup\Option\SelectConfigOption; use Magento\Framework\Setup\Option\TextConfigOption; use Magento\Setup\Validator\RedisConnectionValidator; @@ -26,16 +26,18 @@ class PageCache implements ConfigOptionsListInterface const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER = 'page-cache-redis-server'; const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_DATABASE = 'page-cache-redis-db'; const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PORT = 'page-cache-redis-port'; - const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA = 'page-cache-redis-compress-data'; const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD = 'page-cache-redis-password'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA = 'page-cache-redis-compress-data'; + const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESSION_LIB = 'page-cache-redis-compression-lib'; const INPUT_KEY_PAGE_CACHE_ID_PREFIX = 'page-cache-id-prefix'; const CONFIG_PATH_PAGE_CACHE_BACKEND = 'cache/frontend/page_cache/backend'; const CONFIG_PATH_PAGE_CACHE_BACKEND_SERVER = 'cache/frontend/page_cache/backend_options/server'; const CONFIG_PATH_PAGE_CACHE_BACKEND_DATABASE = 'cache/frontend/page_cache/backend_options/database'; const CONFIG_PATH_PAGE_CACHE_BACKEND_PORT = 'cache/frontend/page_cache/backend_options/port'; - const CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESS_DATA = 'cache/frontend/page_cache/backend_options/compress_data'; const CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD = 'cache/frontend/page_cache/backend_options/password'; + const CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESS_DATA = 'cache/frontend/page_cache/backend_options/compress_data'; + const CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESSION_LIB = 'cache/frontend/page_cache/backend_options/compression_lib'; const CONFIG_PATH_PAGE_CACHE_ID_PREFIX = 'cache/frontend/page_cache/id_prefix'; /** @@ -45,8 +47,9 @@ class PageCache implements ConfigOptionsListInterface self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER => '127.0.0.1', self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_DATABASE => '1', self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PORT => '6379', + self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD => '', self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA => '0', - self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD => '' + self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESSION_LIB => '', ]; /** @@ -63,8 +66,10 @@ class PageCache implements ConfigOptionsListInterface self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_SERVER => self::CONFIG_PATH_PAGE_CACHE_BACKEND_SERVER, self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_DATABASE => self::CONFIG_PATH_PAGE_CACHE_BACKEND_DATABASE, self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PORT => self::CONFIG_PATH_PAGE_CACHE_BACKEND_PORT, + self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD => self::CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD, self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA => self::CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESS_DATA, - self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD => self::CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD + self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESSION_LIB => + self::CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESSION_LIB, ]; /** @@ -113,6 +118,12 @@ public function getOptions() self::CONFIG_PATH_PAGE_CACHE_BACKEND_PORT, 'Redis server listen port' ), + new TextConfigOption( + self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD, + 'Redis server password' + ), new TextConfigOption( self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA, TextConfigOption::FRONTEND_WIZARD_TEXT, @@ -120,17 +131,17 @@ public function getOptions() 'Set to 1 to compress the full page cache (use 0 to disable)' ), new TextConfigOption( - self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD, + self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESSION_LIB, TextConfigOption::FRONTEND_WIZARD_TEXT, - self::CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD, - 'Redis server password' + self::CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESSION_LIB, + 'Compression library to use [snappy,lzf,l4z,zstd,gzip] (leave blank to determine automatically)' ), new TextConfigOption( self::INPUT_KEY_PAGE_CACHE_ID_PREFIX, TextConfigOption::FRONTEND_WIZARD_TEXT, self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, 'ID prefix for cache keys' - ) + ), ]; } @@ -224,7 +235,7 @@ private function validateRedisConfig(array $options, DeploymentConfig $deploymen self::CONFIG_PATH_PAGE_CACHE_BACKEND_DATABASE, $this->getDefaultConfigValue(self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_DATABASE) ); - + $config['password'] = isset($options[self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD]) ? $options[self::INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD] : $deploymentConfig->get( diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php index c0ec78f046e23..e864a81ffcc0e 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php @@ -139,7 +139,7 @@ class Session implements ConfigOptionsListInterface ]; /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getOptions() @@ -289,7 +289,7 @@ public function getOptions() } /** - * {@inheritdoc} + * @inheritdoc */ public function createConfig(array $options, DeploymentConfig $deploymentConfig) { @@ -320,7 +320,7 @@ public function createConfig(array $options, DeploymentConfig $deploymentConfig) } /** - * {@inheritdoc} + * @inheritdoc */ public function validate(array $options, DeploymentConfig $deploymentConfig) { @@ -340,7 +340,7 @@ public function validate(array $options, DeploymentConfig $deploymentConfig) if (isset($options[self::INPUT_KEY_SESSION_REDIS_LOG_LEVEL])) { $level = $options[self::INPUT_KEY_SESSION_REDIS_LOG_LEVEL]; - if (($level < 0) or ($level > 7)) { + if (($level < 0) || ($level > 7)) { $errors[] = "Invalid Redis log level '{$level}'. Valid range is 0-7, inclusive."; } } diff --git a/setup/src/Magento/Setup/Test/Unit/Controller/SessionTest.php b/setup/src/Magento/Setup/Test/Unit/Controller/SessionTest.php index f8e5e7cdc4d70..216013ebfc8d9 100644 --- a/setup/src/Magento/Setup/Test/Unit/Controller/SessionTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Controller/SessionTest.php @@ -48,6 +48,12 @@ public function testUnloginAction() $this->createPartialMock(\Magento\Framework\App\DeploymentConfig::class, ['isAvailable']); $deployConfigMock->expects($this->once())->method('isAvailable')->will($this->returnValue(true)); + $sessionMock = $this->createPartialMock( + \Magento\Backend\Model\Auth\Session::class, + ['prolong', 'isSessionExists'] + ); + $sessionMock->expects($this->once())->method('isSessionExists')->will($this->returnValue(false)); + $stateMock = $this->createPartialMock(\Magento\Framework\App\State::class, ['setAreaCode']); $stateMock->expects($this->once())->method('setAreaCode'); @@ -57,6 +63,7 @@ public function testUnloginAction() $urlMock = $this->createMock(\Magento\Backend\Model\Url::class); $returnValueMap = [ + [\Magento\Backend\Model\Auth\Session::class, $sessionMock], [\Magento\Framework\App\State::class, $stateMock], [\Magento\Backend\Model\Session\AdminConfig::class, $sessionConfigMock], [\Magento\Backend\Model\Url::class, $urlMock] @@ -68,7 +75,6 @@ public function testUnloginAction() ->method('get') ->will($this->returnValueMap($returnValueMap)); - $sessionMock = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['prolong']); $this->objectManager->expects($this->once()) ->method('create') ->will($this->returnValue($sessionMock)); @@ -87,4 +93,29 @@ public function testIndexAction() $viewModel = $controller->unloginAction(); $this->assertInstanceOf(\Zend\View\Model\ViewModel::class, $viewModel); } + + /** + * @covers \Magento\Setup\Controller\SystemConfig::prolongAction + */ + public function testProlongActionWithExistingSession() + { + $this->objectManagerProvider->expects($this->once())->method('get')->will( + $this->returnValue($this->objectManager) + ); + $deployConfigMock = + $this->createPartialMock(\Magento\Framework\App\DeploymentConfig::class, ['isAvailable']); + $deployConfigMock->expects($this->once())->method('isAvailable')->will($this->returnValue(true)); + $sessionMock = $this->createPartialMock( + \Magento\Backend\Model\Auth\Session::class, + ['prolong', 'isSessionExists'] + ); + $sessionMock->expects($this->once())->method('isSessionExists')->will($this->returnValue(true)); + + $this->serviceManager->expects($this->once())->method('get')->will($this->returnValue($deployConfigMock)); + $this->objectManager->expects($this->once()) + ->method('get') + ->will($this->returnValue($sessionMock)); + $controller = new Session($this->serviceManager, $this->objectManagerProvider); + $this->assertEquals(new \Zend\View\Model\JsonModel(['success' => true]), $controller->prolongAction()); + } } diff --git a/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php b/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php new file mode 100644 index 0000000000000..e28415961f26a --- /dev/null +++ b/setup/src/Magento/Setup/Test/Unit/Fixtures/CouponCodesFixtureTest.php @@ -0,0 +1,198 @@ +fixtureModelMock = $this->createMock(\Magento\Setup\Fixtures\FixtureModel::class); + $this->ruleFactoryMock = $this->createPartialMock(\Magento\SalesRule\Model\RuleFactory::class, ['create']); + $this->couponCodeFactoryMock = $this->createPartialMock( + \Magento\SalesRule\Model\CouponFactory::class, + ['create'] + ); + $this->model = new CouponCodesFixture( + $this->fixtureModelMock, + $this->ruleFactoryMock, + $this->couponCodeFactoryMock + ); + } + + /** + * testExecute + */ + public function testExecute() + { + $storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $storeMock->expects($this->once()) + ->method('getRootCategoryId') + ->will($this->returnValue(2)); + + $websiteMock = $this->createMock(\Magento\Store\Model\Website::class); + $websiteMock->expects($this->once()) + ->method('getGroups') + ->will($this->returnValue([$storeMock])); + $websiteMock->expects($this->once()) + ->method('getId') + ->will($this->returnValue('website_id')); + + $contextMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\Context::class); + $abstractDbMock = $this->getMockForAbstractClass( + \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, + [$contextMock], + '', + true, + true, + true, + ['getAllChildren'] + ); + $abstractDbMock->expects($this->once()) + ->method('getAllChildren') + ->will($this->returnValue([1])); + + $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManager::class); + $storeManagerMock->expects($this->once()) + ->method('getWebsites') + ->will($this->returnValue([$websiteMock])); + + $categoryMock = $this->createMock(\Magento\Catalog\Model\Category::class); + $categoryMock->expects($this->once()) + ->method('getResource') + ->will($this->returnValue($abstractDbMock)); + $categoryMock->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('path/to/file')); + $categoryMock->expects($this->once()) + ->method('getId') + ->will($this->returnValue('category_id')); + + $objectValueMap = [ + [\Magento\Catalog\Model\Category::class, $categoryMock] + ]; + + $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManager\ObjectManager::class); + $objectManagerMock->expects($this->once()) + ->method('create') + ->will($this->returnValue($storeManagerMock)); + $objectManagerMock->expects($this->once()) + ->method('get') + ->will($this->returnValueMap($objectValueMap)); + + $valueMap = [ + ['coupon_codes', 0, 1] + ]; + + $this->fixtureModelMock + ->expects($this->exactly(1)) + ->method('getValue') + ->will($this->returnValueMap($valueMap)); + $this->fixtureModelMock + ->expects($this->exactly(2)) + ->method('getObjectManager') + ->will($this->returnValue($objectManagerMock)); + + $ruleMock = $this->createMock(\Magento\SalesRule\Model\Rule::class); + $this->ruleFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($ruleMock); + + $couponMock = $this->createMock(\Magento\SalesRule\Model\Coupon::class); + $couponMock->expects($this->once()) + ->method('setRuleId') + ->willReturnSelf(); + $couponMock->expects($this->once()) + ->method('setCode') + ->willReturnSelf(); + $couponMock->expects($this->once()) + ->method('setIsPrimary') + ->willReturnSelf(); + $couponMock->expects($this->once()) + ->method('setType') + ->willReturnSelf(); + $couponMock->expects($this->once()) + ->method('save') + ->willReturnSelf(); + $this->couponCodeFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($couponMock); + + $this->model->execute(); + } + + /** + * testNoFixtureConfigValue + */ + public function testNoFixtureConfigValue() + { + $ruleMock = $this->createMock(\Magento\SalesRule\Model\Rule::class); + $ruleMock->expects($this->never())->method('save'); + + $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManager\ObjectManager::class); + $objectManagerMock->expects($this->never()) + ->method('get') + ->with($this->equalTo(\Magento\SalesRule\Model\Rule::class)) + ->willReturn($ruleMock); + + $this->fixtureModelMock + ->expects($this->never()) + ->method('getObjectManager') + ->willReturn($objectManagerMock); + $this->fixtureModelMock + ->expects($this->once()) + ->method('getValue') + ->willReturn(false); + + $this->model->execute(); + } + + /** + * testGetActionTitle + */ + public function testGetActionTitle() + { + $this->assertSame('Generating coupon codes', $this->model->getActionTitle()); + } + + /** + * testIntroduceParamLabels + */ + public function testIntroduceParamLabels() + { + $this->assertSame([ + 'coupon_codes' => 'Coupon Codes' + ], $this->model->introduceParamLabels()); + } +} diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php index 9c123fcb330dd..783c11e941eed 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php @@ -45,7 +45,7 @@ protected function setUp() public function testGetOptions() { $options = $this->configOptionsList->getOptions(); - $this->assertCount(6, $options); + $this->assertCount(8, $options); $this->assertArrayHasKey(0, $options); $this->assertInstanceOf(SelectConfigOption::class, $options[0]); @@ -69,7 +69,15 @@ public function testGetOptions() $this->assertArrayHasKey(5, $options); $this->assertInstanceOf(TextConfigOption::class, $options[5]); - $this->assertEquals('cache-id-prefix', $options[5]->getName()); + $this->assertEquals('cache-backend-redis-compress-data', $options[5]->getName()); + + $this->assertArrayHasKey(6, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[6]); + $this->assertEquals('cache-backend-redis-compression-lib', $options[6]->getName()); + + $this->assertArrayHasKey(7, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[7]); + $this->assertEquals('cache-id-prefix', $options[7]->getName()); } /** @@ -88,7 +96,9 @@ public function testCreateConfigCacheRedis() 'server' => '', 'port' => '', 'database' => '', - 'password' => '' + 'password' => '', + 'compress_data' => '', + 'compression_lib' => '', ], 'id_prefix' => $this->expectedIdPrefix(), ] @@ -115,18 +125,23 @@ public function testCreateConfigWithRedisConfig() 'server' => 'localhost', 'port' => '1234', 'database' => '5', - 'password' => '' + 'password' => '', + 'compress_data' => '1', + 'compression_lib' => 'gzip', ], 'id_prefix' => $this->expectedIdPrefix(), ] ] ] ]; + $options = [ 'cache-backend' => 'redis', 'cache-backend-redis-server' => 'localhost', 'cache-backend-redis-port' => '1234', - 'cache-backend-redis-db' => '5' + 'cache-backend-redis-db' => '5', + 'cache-backend-redis-compress-data' => '1', + 'cache-backend-redis-compression-lib' => 'gzip' ]; $configData = $this->configOptionsList->createConfig($options, $this->deploymentConfigMock); diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php new file mode 100644 index 0000000000000..1a46bddf5f21a --- /dev/null +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php @@ -0,0 +1,232 @@ +deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->lockConfigOptionsList = new LockConfigOptionsList(); + } + + /** + * @return void + */ + public function testGetOptions() + { + $options = $this->lockConfigOptionsList->getOptions(); + $this->assertSame(5, count($options)); + + $this->assertArrayHasKey(0, $options); + $this->assertInstanceOf(SelectConfigOption::class, $options[0]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER, $options[0]->getName()); + + $this->assertArrayHasKey(1, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[1]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_DB_PREFIX, $options[1]->getName()); + + $this->assertArrayHasKey(2, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[2]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST, $options[2]->getName()); + + $this->assertArrayHasKey(3, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[3]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH, $options[3]->getName()); + + $this->assertArrayHasKey(4, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[4]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH, $options[4]->getName()); + } + + /** + * @param array $options + * @param array $expectedResult + * @dataProvider createConfigDataProvider + */ + public function testCreateConfig(array $options, array $expectedResult) + { + $this->deploymentConfigMock->expects($this->any()) + ->method('get') + ->willReturnArgument(1); + $data = $this->lockConfigOptionsList->createConfig($options, $this->deploymentConfigMock); + $this->assertInstanceOf(ConfigData::class, $data); + $this->assertTrue($data->isOverrideWhenSave()); + $this->assertSame($expectedResult, $data->getData()); + } + + /** + * @return array + */ + public function createConfigDataProvider(): array + { + return [ + 'Check default values' => [ + 'options' => [], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_DB, + 'config' => [ + 'prefix' => null, + ], + ], + ], + ], + 'Check default value for cache lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_CACHE, + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_CACHE, + ], + ], + ], + 'Check default value for zookeeper lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'config' => [ + 'host' => null, + 'path' => ZookeeperLock::DEFAULT_PATH, + ], + ], + ], + ], + 'Check specific db lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, + LockConfigOptionsList::INPUT_KEY_LOCK_DB_PREFIX => 'my_prefix' + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_DB, + 'config' => [ + 'prefix' => 'my_prefix', + ], + ], + ], + ], + 'Check specific zookeeper lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST => '123.45.67.89:10', + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH => '/some/path', + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'config' => [ + 'host' => '123.45.67.89:10', + 'path' => '/some/path', + ], + ], + ], + ], + 'Check specific file lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_FILE, + LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH => '/my/path' + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_FILE, + 'config' => [ + 'path' => '/my/path', + ], + ], + ], + ], + ]; + } + + /** + * @param array $options + * @param array $expectedResult + * @dataProvider validateDataProvider + */ + public function testValidate(array $options, array $expectedResult) + { + $this->deploymentConfigMock->expects($this->any()) + ->method('get') + ->willReturnArgument(1); + $this->assertSame( + $expectedResult, + $this->lockConfigOptionsList->validate($options, $this->deploymentConfigMock) + ); + } + + /** + * @return array + */ + public function validateDataProvider(): array + { + return [ + 'Wrong lock provider' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => 'SomeProvider', + ], + 'expectedResult' => [ + 'The lock provider SomeProvider does not exist.', + ], + ], + 'Empty host and path for Zookeeper' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST => '', + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH => '', + ], + 'expectedResult' => extension_loaded('zookeeper') + ? [ + 'Zookeeper path needs to be a non-empty string.', + 'Zookeeper host is should be set.', + ] + : [ + 'php extension Zookeeper is not installed.', + 'Zookeeper path needs to be a non-empty string.', + 'Zookeeper host is should be set.', + ], + ], + 'Empty path for File lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_FILE, + LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH => '', + ], + 'expectedResult' => [ + 'The path needs to be a non-empty string.', + ], + ], + ]; + } +} diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php index 1cf3937f98684..673168fe2fffd 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php @@ -45,7 +45,7 @@ protected function setUp() public function testGetOptions() { $options = $this->configList->getOptions(); - $this->assertCount(7, $options); + $this->assertCount(8, $options); $this->assertArrayHasKey(0, $options); $this->assertInstanceOf(SelectConfigOption::class, $options[0]); @@ -65,15 +65,19 @@ public function testGetOptions() $this->assertArrayHasKey(4, $options); $this->assertInstanceOf(TextConfigOption::class, $options[4]); - $this->assertEquals('page-cache-redis-compress-data', $options[4]->getName()); + $this->assertEquals('page-cache-redis-password', $options[4]->getName()); $this->assertArrayHasKey(5, $options); $this->assertInstanceOf(TextConfigOption::class, $options[5]); - $this->assertEquals('page-cache-redis-password', $options[5]->getName()); + $this->assertEquals('page-cache-redis-compress-data', $options[5]->getName()); $this->assertArrayHasKey(6, $options); $this->assertInstanceOf(TextConfigOption::class, $options[6]); - $this->assertEquals('page-cache-id-prefix', $options[6]->getName()); + $this->assertEquals('page-cache-redis-compression-lib', $options[6]->getName()); + + $this->assertArrayHasKey(7, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[7]); + $this->assertEquals('page-cache-id-prefix', $options[7]->getName()); } /** @@ -93,7 +97,8 @@ public function testCreateConfigWithRedis() 'port' => '', 'database' => '', 'compress_data' => '', - 'password' => '' + 'password' => '', + 'compression_lib' => '', ], 'id_prefix' => $this->expectedIdPrefix(), ] @@ -120,8 +125,9 @@ public function testCreateConfigWithRedisConfiguration() 'server' => 'foo.bar', 'port' => '9000', 'database' => '6', + 'password' => '', 'compress_data' => '1', - 'password' => '' + 'compression_lib' => 'gzip', ], 'id_prefix' => $this->expectedIdPrefix(), ] @@ -134,7 +140,8 @@ public function testCreateConfigWithRedisConfiguration() 'page-cache-redis-server' => 'foo.bar', 'page-cache-redis-port' => '9000', 'page-cache-redis-db' => '6', - 'page-cache-redis-compress-data' => '1' + 'page-cache-redis-compress-data' => '1', + 'page-cache-redis-compression-lib' => 'gzip', ]; $configData = $this->configList->createConfig($options, $this->deploymentConfigMock); diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php index f342a11493498..a85b468cebc92 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php @@ -7,6 +7,7 @@ namespace Magento\Setup\Test\Unit\Model; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Setup\Model\ConfigOptionsList\Lock; use Magento\Setup\Model\ConfigGenerator; use Magento\Setup\Model\ConfigOptionsList; use Magento\Setup\Validator\DbValidator; @@ -82,7 +83,7 @@ public function testCreateOptions() $this->generator->expects($this->once())->method('createXFrameConfig')->willReturn($configDataMock); $this->generator->expects($this->once())->method('createCacheHostsConfig')->willReturn($configDataMock); - $configData = $this->object->createConfig([], $this->deploymentConfig); + $configData = $this->object->createConfig([Lock::INPUT_KEY_LOCK_PROVIDER => 'db'], $this->deploymentConfig); $this->assertGreaterThanOrEqual(6, count($configData)); } @@ -96,7 +97,7 @@ public function testCreateOptionsWithOptionalNull() $this->generator->expects($this->once())->method('createXFrameConfig')->willReturn($configDataMock); $this->generator->expects($this->once())->method('createCacheHostsConfig')->willReturn($configDataMock); - $configData = $this->object->createConfig([], $this->deploymentConfig); + $configData = $this->object->createConfig([Lock::INPUT_KEY_LOCK_PROVIDER => 'db'], $this->deploymentConfig); $this->assertGreaterThanOrEqual(6, count($configData)); } @@ -109,7 +110,8 @@ public function testValidateSuccess() ConfigOptionsListConstants::INPUT_KEY_DB_NAME => 'name', ConfigOptionsListConstants::INPUT_KEY_DB_HOST => 'host', ConfigOptionsListConstants::INPUT_KEY_DB_USER => 'user', - ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass' + ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->prepareValidationMocks(); @@ -127,7 +129,8 @@ public function testValidateInvalidSessionHandler() ConfigOptionsListConstants::INPUT_KEY_DB_NAME => 'name', ConfigOptionsListConstants::INPUT_KEY_DB_HOST => 'host', ConfigOptionsListConstants::INPUT_KEY_DB_USER => 'user', - ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass' + ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->prepareValidationMocks(); @@ -141,10 +144,11 @@ public function testValidateEmptyEncryptionKey() { $options = [ ConfigOptionsListConstants::INPUT_KEY_SKIP_DB_VALIDATION => true, - ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '' + ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->assertEquals( - ['Invalid encryption key'], + ['Invalid encryption key. Encryption key must be 32 character string without any white space.'], $this->object->validate($options, $this->deploymentConfig) ); } @@ -167,7 +171,8 @@ public function testValidateCacheHosts($hosts, $expectedError) { $options = [ ConfigOptionsListConstants::INPUT_KEY_SKIP_DB_VALIDATION => true, - ConfigOptionsListConstants::INPUT_KEY_CACHE_HOSTS => $hosts + ConfigOptionsListConstants::INPUT_KEY_CACHE_HOSTS => $hosts, + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $result = $this->object->validate($options, $this->deploymentConfig); if ($expectedError) { diff --git a/setup/src/Magento/Setup/Validator/DbValidator.php b/setup/src/Magento/Setup/Validator/DbValidator.php index 1eae39160ff8e..95a1fef395b13 100644 --- a/setup/src/Magento/Setup/Validator/DbValidator.php +++ b/setup/src/Magento/Setup/Validator/DbValidator.php @@ -166,7 +166,7 @@ private function checkDatabasePrivileges(\Magento\Framework\DB\Adapter\AdapterIn return true; } - // check table privileges + // check database privileges $schemaPrivilegesQuery = "SELECT PRIVILEGE_TYPE FROM SCHEMA_PRIVILEGES " . "WHERE '$dbName' LIKE TABLE_SCHEMA AND REPLACE(GRANTEE, '\'', '') = current_user()"; $grantInfo = $connection->query($schemaPrivilegesQuery)->fetchAll(\PDO::FETCH_NUM); @@ -175,7 +175,7 @@ private function checkDatabasePrivileges(\Magento\Framework\DB\Adapter\AdapterIn } $errorMessage = 'Database user does not have enough privileges. Please make sure ' - . implode(', ', $requiredPrivileges) . " privileges are granted to table '{$dbName}'."; + . implode(', ', $requiredPrivileges) . " privileges are granted to database '{$dbName}'."; throw new \Magento\Setup\Exception($errorMessage); } diff --git a/setup/view/magento/setup/web-configuration.phtml b/setup/view/magento/setup/web-configuration.phtml index d0307a71e4a37..4018694dfd0c3 100644 --- a/setup/view/magento/setup/web-configuration.phtml +++ b/setup/view/magento/setup/web-configuration.phtml @@ -282,15 +282,20 @@ $hints = [ tooltip-html-unsafe="" tooltip-trigger="focus" tooltip-append-to-body="true" - ng-minlength="4" + ng-minlength="32" + ng-maxlength="32" + ng-pattern="/^\S+$/" required >
You must enter an encryption key. - - Your encryption key must be longer and stronger. + + Encryption key must be 32 character string without any white space.
diff --git a/setup/view/styles/lib/variables/_colors.less b/setup/view/styles/lib/variables/_colors.less index 638490ac8673a..a72dc69ac7669 100644 --- a/setup/view/styles/lib/variables/_colors.less +++ b/setup/view/styles/lib/variables/_colors.less @@ -24,7 +24,7 @@ @color-green-apple: #79a22e; @color-green-islamic: #090; @color-dark-brownie: #41362f; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-phoenix-down: #e04f00; @color-phoenix: #eb5202; @color-phoenix-almost-rise: #ef672f;